Full Customization
Build your own UI with complete design control
React Hooks
Access consent state with useConsent hook
Cross-Device Sync
Preferences sync across all user devices
Accessible
WCAG 2.1 compliant patterns included
SDK Hooks for Consent State
The useConsent hook provides everything you need to build a custom preference center.
import { useConsent } from '@sylphx/sdk/react'
function PreferenceCenter() {
const {
// Current consent state
consents, // { analytics: true, marketing: false, ... }
consentTypes, // Array of configured consent categories
hasResponded, // Whether user has made any choice
isLoading, // Loading state
lastUpdated, // Timestamp of last update
// Privacy signals
gpcEnabled, // Global Privacy Control detected
dntEnabled, // Do Not Track detected
// Methods
hasConsent, // Check single: hasConsent('analytics')
setConsent, // Set single: setConsent('analytics', true)
setConsents, // Set multiple: setConsents({ analytics: true })
acceptAll, // Accept all categories
declineOptional, // Decline non-required
resetConsent, // Clear all (re-show banner)
// Audit
consentHistory, // Array of consent change records
} = useConsent()
// Build your custom UI...
}ConsentType Interface
| Property | Type | Description |
|---|---|---|
idrequired | string | Unique identifier for the consent category |
namerequired | string | Display name for the category |
descriptionrequired | string | Explanation of what this consent covers |
required | boolean= false | Whether this consent is mandatory (always enabled) |
granted | boolean | Current consent state for this category |
cookies | string[] | List of cookie names in this category |
scripts | string[] | Third-party script domains in this category |
Building a Custom Preference Center
Here's a complete example of a custom preference center with granular controls:
'use client'
import { useConsent } from '@sylphx/sdk/react'
import { useState } from 'react'
export function PreferenceCenter() {
const {
consentTypes,
consents,
setConsents,
acceptAll,
declineOptional,
gpcEnabled,
lastUpdated,
isLoading,
} = useConsent()
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({})
const [isSaving, setIsSaving] = useState(false)
// Track local changes before saving
const handleToggle = (categoryId: string, value: boolean) => {
setPendingChanges(prev => ({ ...prev, [categoryId]: value }))
}
// Get current value (pending change or saved)
const getValue = (categoryId: string) => {
return pendingChanges[categoryId] ?? consents[categoryId] ?? false
}
// Save all pending changes
const handleSave = async () => {
if (Object.keys(pendingChanges).length === 0) return
setIsSaving(true)
try {
await setConsents(pendingChanges)
setPendingChanges({})
} finally {
setIsSaving(false)
}
}
// Check if there are unsaved changes
const hasChanges = Object.keys(pendingChanges).length > 0
if (isLoading) {
return <div className="animate-pulse">Loading preferences...</div>
}
return (
<div className="max-w-2xl mx-auto p-6 bg-background rounded-xl border border-border">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-foreground">Privacy Preferences</h2>
<p className="text-muted-foreground mt-1">
Manage how we use your data. Changes take effect immediately.
</p>
</div>
{/* GPC Notice */}
{gpcEnabled && (
<div className="mb-6 p-4 bg-success/10 border border-success/30 rounded-lg">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-success" />
<span className="font-medium text-success">Global Privacy Control Detected</span>
</div>
<p className="text-sm text-muted-foreground mt-1">
Your browser has GPC enabled. We respect this signal and have limited data sharing.
</p>
</div>
)}
{/* Quick Actions */}
<div className="flex gap-3 mb-6">
<button
onClick={acceptAll}
className="flex-1 py-2 px-4 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Accept All
</button>
<button
onClick={declineOptional}
className="flex-1 py-2 px-4 border border-border rounded-lg font-medium hover:bg-muted transition-colors"
>
Essential Only
</button>
</div>
{/* Category List */}
<div className="space-y-4">
{consentTypes.map((type) => (
<div
key={type.id}
className={`p-4 rounded-lg border transition-colors ${
type.required
? 'border-border bg-muted/50'
: 'border-border hover:border-primary/30'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-foreground">{type.name}</h3>
{type.required && (
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
Required
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">{type.description}</p>
{/* Show affected cookies/scripts */}
{type.cookies && type.cookies.length > 0 && (
<div className="mt-2">
<span className="text-xs text-muted-foreground">Cookies: </span>
<span className="text-xs font-mono text-muted-foreground">
{type.cookies.join(', ')}
</span>
</div>
)}
</div>
{/* Toggle Switch */}
<button
role="switch"
aria-checked={getValue(type.id)}
aria-label={`${type.name} consent`}
disabled={type.required}
onClick={() => handleToggle(type.id, !getValue(type.id))}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
getValue(type.id)
? 'bg-primary'
: 'bg-muted'
} ${type.required ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
getValue(type.id) ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
</div>
))}
</div>
{/* Save Button */}
{hasChanges && (
<div className="mt-6 flex items-center justify-between p-4 bg-muted rounded-lg">
<span className="text-sm text-muted-foreground">You have unsaved changes</span>
<button
onClick={handleSave}
disabled={isSaving}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
>
{isSaving ? 'Saving...' : 'Save Preferences'}
</button>
</div>
)}
{/* Last Updated */}
{lastUpdated && (
<p className="mt-4 text-xs text-muted-foreground text-center">
Last updated: {new Date(lastUpdated).toLocaleDateString()}
</p>
)}
</div>
)
}Styling the Preference UI
Create different visual styles for your preference center to match your brand:
// Minimal, list-based preference UI
export function MinimalPreferences() {
const { consentTypes, setConsent } = useConsent()
return (
<div className="divide-y divide-border">
{consentTypes.map((type) => (
<label
key={type.id}
className="flex items-center justify-between py-4 cursor-pointer"
>
<div>
<span className="font-medium text-foreground">{type.name}</span>
<p className="text-sm text-muted-foreground">{type.description}</p>
</div>
<input
type="checkbox"
checked={type.granted}
disabled={type.required}
onChange={(e) => setConsent(type.id, e.target.checked)}
className="h-5 w-5 rounded border-border text-primary focus:ring-primary"
/>
</label>
))}
</div>
)
}Handling Granular Consent
Implement granular consent controls for different purposes like analytics, marketing, and functional cookies:
// Configure granular consent categories in your dashboard or code
const consentCategories = [
{
id: 'necessary',
name: 'Essential',
description: 'Required for the website to function properly',
required: true,
cookies: ['session', 'csrf_token', 'consent_preferences'],
},
{
id: 'analytics',
name: 'Analytics',
description: 'Help us understand how visitors use our site',
required: false,
cookies: ['_ga', '_gid', '_gat'],
scripts: ['google-analytics.com', 'plausible.io'],
},
{
id: 'marketing',
name: 'Marketing & Advertising',
description: 'Used for targeted advertising and remarketing',
required: false,
cookies: ['_fbp', '_gcl_au', 'ads_prefs'],
scripts: ['facebook.com', 'doubleclick.net'],
},
{
id: 'functional',
name: 'Functional',
description: 'Enable features like live chat and video players',
required: false,
cookies: ['chat_session', 'video_quality'],
scripts: ['intercom.io', 'vimeo.com'],
},
{
id: 'social',
name: 'Social Media',
description: 'Enable social sharing and embedded content',
required: false,
cookies: ['twitter_sess', 'linkedin_oauth'],
scripts: ['twitter.com', 'linkedin.com'],
},
]'use client'
import { useConsent } from '@sylphx/sdk/react'
import Script from 'next/script'
export function ConditionalScripts() {
const { hasConsent, isLoading } = useConsent()
// Don't render anything until consent state is loaded
if (isLoading) return null
return (
<>
{/* Analytics - only load if consented */}
{hasConsent('analytics') && (
<>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXX', {
anonymize_ip: true,
});
`}
</Script>
</>
)}
{/* Marketing - Facebook Pixel */}
{hasConsent('marketing') && (
<Script id="fb-pixel" strategy="afterInteractive">
{`
!function(f,b,e,v,n,t,s){...}
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
`}
</Script>
)}
{/* Functional - Live Chat */}
{hasConsent('functional') && (
<Script id="intercom" strategy="afterInteractive">
{`
window.intercomSettings = { app_id: 'YOUR_APP_ID' };
// Intercom snippet...
`}
</Script>
)}
</>
)
}
// Use the consent check in event handlers
export function TrackingButton() {
const { hasConsent } = useConsent()
const handleClick = () => {
// Only track if user consented to analytics
if (hasConsent('analytics')) {
gtag('event', 'button_click', { category: 'engagement' })
}
// Only fire conversion if marketing consent given
if (hasConsent('marketing')) {
fbq('track', 'Lead')
}
// Core functionality always works
performAction()
}
return <button onClick={handleClick}>Get Started</button>
}Syncing Preferences Across Devices
When users are authenticated, their consent preferences automatically sync across all their devices:
'use client'
import { useConsent, useAuth } from '@sylphx/sdk/react'
import { useEffect } from 'react'
export function ConsentProvider({ children }) {
const { user, isAuthenticated } = useAuth()
const { syncWithServer, consents } = useConsent()
useEffect(() => {
if (isAuthenticated && user) {
// When user logs in, sync consent from server
// This ensures preferences set on one device apply everywhere
syncWithServer({
userId: user.id,
// Strategy for handling conflicts:
// - 'server': Server preferences override local
// - 'local': Local preferences override server
// - 'merge': Most restrictive wins (recommended for privacy)
conflictStrategy: 'merge',
})
}
}, [isAuthenticated, user, syncWithServer])
return <>{children}</>
}
// On the server, preferences are automatically stored with the user
// The SDK handles syncing transparently:
//
// 1. User sets preferences on Desktop -> stored locally + server
// 2. User logs in on Mobile -> local synced from server
// 3. User changes preference on Mobile -> updated locally + server
// 4. Desktop automatically receives update on next page loadimport { platform } from '@/lib/platform'
// Manually sync preferences to server (for authenticated users)
export async function syncConsentToServer(userId: string, consents: Record<string, boolean>) {
await platform.consent.setConsents({
userId,
consents,
// Include metadata for audit trail
metadata: {
timestamp: new Date().toISOString(),
source: 'preference_center',
userAgent: navigator.userAgent,
},
})
}
// Fetch preferences from server (on login)
export async function fetchConsentFromServer(userId: string) {
const serverConsents = await platform.consent.getUserConsents(userId)
// Returns:
// {
// consents: { analytics: true, marketing: false, ... },
// lastUpdated: '2024-01-15T10:30:00Z',
// gpcEnabled: false,
// }
return serverConsents
}Anonymous Users
Best Practices
Clear Category Descriptions
Use plain language to explain what each category does. Avoid technical jargon.
Immediate Feedback
Show visual confirmation when preferences are saved. Use optimistic updates for better UX.
Respect User Choices
Never pre-select non-essential categories. Always honor opt-out preferences.
Easy Access
Make the preference center accessible from footer links, account settings, and the cookie banner.
Mobile-Friendly
Ensure the preference center works well on all screen sizes. Use touch-friendly controls.
Show Affected Items
List the specific cookies and scripts affected by each category for transparency.
Accessibility Considerations
Ensure your preference center is accessible to all users, including those using assistive technologies:
'use client'
interface AccessibleToggleProps {
id: string
label: string
description: string
checked: boolean
disabled?: boolean
onChange: (checked: boolean) => void
}
export function AccessibleToggle({
id,
label,
description,
checked,
disabled,
onChange,
}: AccessibleToggleProps) {
const descriptionId = `${id}-description`
return (
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<label
htmlFor={id}
className="font-medium text-foreground cursor-pointer"
>
{label}
</label>
<p id={descriptionId} className="text-sm text-muted-foreground">
{description}
</p>
</div>
<button
id={id}
role="switch"
aria-checked={checked}
aria-describedby={descriptionId}
aria-disabled={disabled}
disabled={disabled}
onClick={() => onChange(!checked)}
onKeyDown={(e) => {
// Support Space and Enter keys
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
if (!disabled) onChange(!checked)
}
}}
className={`
relative inline-flex h-6 w-11 items-center rounded-full
transition-colors duration-200 ease-in-out
focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
${checked ? 'bg-primary' : 'bg-muted'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span className="sr-only">
{checked ? 'Disable' : 'Enable'} {label}
</span>
<span
aria-hidden="true"
className={`
inline-block h-4 w-4 transform rounded-full bg-white shadow
transition-transform duration-200 ease-in-out
${checked ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
)
}Accessibility Checklist
- Use semantic HTML (buttons, labels)
- Add ARIA attributes for switches
- Ensure keyboard navigation works
- Provide visible focus indicators
- Use sufficient color contrast
- Include descriptive labels
- Announce state changes to screen readers
- Test with assistive technologies
Screen Reader Announcements
aria-live regions to announce when preferences are saved or when errors occur. This ensures screen reader users receive feedback about their actions.Need pre-built components?
Use our ready-made CookieBanner and ConsentPreferences components for faster implementation.
View Components