Skip to main content

Next.js Components

ConsentDialogTrigger

Add the trigger next to the banner

ConsentDialogTrigger is a draggable floating button that opens the preference center. Place it once, next to the banner and dialog, so visitors can revisit their choices without a footer link.

Keep the boundary and transport from your router setup. Add this component as a child of the existing boundary, alongside its banner and dialog:

components/privacy-trigger.tsx
'use client';

import { ConsentDialogTrigger } from 'c15t/next';

export function PrivacyTrigger() {
	return <ConsentDialogTrigger showWhen="after-prompt" />;
}

showWhen="after-prompt" keeps the button out of the way while a choice or notice is still owed and shows it once the visitor has answered, so the banner and the trigger never compete for the same corner. The default, always, shows it whenever the preference center is closed.

The button carries data-c15t-rights with the rights the active policy guarantees, so a stylesheet can label it differently under an opt-out rule.

Props

PropTypeDefaultDescription
icon'branding' | 'fingerprint' | 'settings' | ReactNode'branding'Icon rendered inside the button.
defaultPosition'bottom-left' | 'bottom-right' | 'top-left' | 'top-right''bottom-right'Corner the button starts in.
persistPositionbooleantrueRemember the corner the visitor dragged it to.
showWhen'always' | 'after-prompt' | 'never''always'When the button is visible. after-prompt waits until no choice or notice is owed.
size'sm' | 'md' | 'lg''md'Button size.
ariaLabelstring'Open privacy settings'Accessible name.
noStylebooleanfalseRemove the default styling.

Configurable toolbar

Use ConsentDialogTriggerToolbar when you want app-owned controls beside the privacy trigger. It always renders exactly one built-in action that opens the preference center, so actions only holds controls your app owns.

This partial example belongs in a Client Component inside the existing boundary. Define the theme state, icons and callbacks in that component.

import { ConsentDialogTriggerToolbar } from 'c15t/next';

<ConsentDialogTriggerToolbar
	ariaLabel="Site controls"
	actions={[
		{
			id: 'theme',
			label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
			icon: isDark ? <SunIcon /> : <MoonIcon />,
			pressed: isDark,
			onSelect: toggleColorScheme,
		},
		{
			id: 'support',
			label: 'Open support chat',
			icon: <ChatIcon />,
			onSelect: openSupportChat,
		},
	]}
	preferences={{ icon: 'fingerprint', label: 'Manage privacy settings' }}
/>;

Each custom action needs a stable id, an accessible label, an icon, and an onSelect callback. Use pressed for toggle actions and disabled for unavailable ones. Your app owns the state behind each action. The preferences action moves to the edge nearest the toolbar's snapped corner while custom actions keep their configured order.

The toolbar does not depend on the consent policy. Your actions always render, so a theme toggle stays available while a banner is up. On the toolbar, showWhen applies only to the built-in preferences action: after-prompt hides that one item while a choice or notice is owed and shows it once the visitor has answered, and never leaves it out. The toolbar renders nothing only when it has no visible item. On the single ConsentDialogTrigger, showWhen applies to the whole control.

No policy, no UI. When no rule has resolved, because resolution failed, no rule matched and you set no default, or init is still withheld, the built-in preferences action and the single ConsentDialogTrigger render nothing; app-owned toolbar actions still do. They appear as soon as a rule resolves, without a remount. A rule with model: 'none' owes no rights, so the built-in action and the single trigger hide under it too, unless the rule adds rights: ['preferences']. The policy chooses what the control says, you choose where and when it sits, and it disappears only when there is nothing to manage.

The built-in action names the strongest right the active rule guarantees. Under a rule that carries the opt-out right, such as a US opt-out notice, its accessible name is the translated "Do not sell or share my data" and the button carries data-right="opt-out". Under any other rule it reads "Manage preferences" with data-right="preferences". Either way it opens the preference center, and a preferences.label you pass replaces the default. The button also carries data-c15t-rights with every right on the rule.

Toolbars are horizontal by default. Set orientation="vertical" to stack the actions and switch arrow-key navigation to up and down:

<ConsentDialogTriggerToolbar orientation="vertical" actions={toolbarActions} />;

Toolbar props

PropTypeDefaultDescription
actionsConsentDialogTriggerToolbarAction[][]App-owned actions rendered beside the preferences action.
preferencesConsentDialogTriggerToolbarPreferences{}Overrides for the built-in action: icon, label, onSelect, className, style. The label defaults to the opt-out or preferences right on the active rule.
orientation'horizontal' | 'vertical''horizontal'Layout direction.
defaultPositionCornerPosition'bottom-right'Corner the toolbar starts in.
persistPositionbooleantrueRemember the dragged corner.
showWhen'always' | 'after-prompt' | 'never''always'When the built-in preferences action is visible. App-owned actions always render. after-prompt waits until no choice or notice is owed.
size'sm' | 'md' | 'lg''md'Size of each action.
ariaLabelstring'Privacy controls'Accessible name for the toolbar group.
noStylebooleanfalseRemove the default styling.

While <ConsentDevTools> is mounted, the toolbar adds a DevTools button at the end farthest from its corner and opens the DevTools panel beside itself. The ready-made ConsentDialogTrigger switches to this toolbar layout for as long as DevTools is mounted.

Toolbar styling

The toolbar follows the standard precedence: bundled styles, then provider slots, then direct className and style on the toolbar or on one action. Use noStyle for a fully custom implementation.

Add these slot values to options.components on your existing ConsentRoot or ConsentProvider. Merge them with any existing component slots and retain the configured transport:

const triggerSlots = {
	toolbar: { className: 'my-toolbar' },
	toolbarItem: { className: 'my-toolbar-item' },
	toolbarIcon: { className: 'my-toolbar-icon' },
};

// In the existing options.components object:
// trigger: triggerSlots

For one toolbar, pass className="fixed-toolbar" directly. Each app-owned item also accepts className, such as className: 'support-action' on your support action. Keep its icon and callback in the Client Component.

The toolbar slot keys are trigger.toolbar, trigger.toolbarItem, and trigger.toolbarIcon. With noStyle, style the current position and interaction state through data-corner, data-dragging, and data-snapping on the toolbar element.