TanStack Start Components
ConsentWidget
Embed the preference center in a page
ConsentWidget renders the category switches and the Reject All, Accept All
and Save Settings actions in the page flow instead of a modal. Use it on a privacy or
settings route. Keep ConsentDialog in the root route too, so the banner's
Customize button still has a dialog to open.
The widget uses ConsentRoot's policy, translations and backend. It needs no
second provider. useConsentDraft and ConsentDraftProvider also import from
c15t/tanstack-start.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
hideBranding | boolean | true | A standalone widget hides the "Secured by" tag. Pass false to show it. |
uiSource | string | 'widget' | Source identifier recorded with saves made from this widget. Inside ConsentDialog it inherits 'dialog'. |
disableAnimation | boolean | false | Skips animations in the widget's parts. |
noStyle | boolean | false | Removes the built-in styling from every part. |
Behavior
The widget is the preference center without the modal around it: an
accordion with one row per category, each with a switch, followed by the
Reject All, Accept All and Save Settings actions. ConsentDialog renders this same
widget inside its card. Standalone, the widget renders in place, takes part
in server rendering, and does not touch the active surface, so it suits a
privacy page or an account settings screen.
The rows are necessary plus the categories in the active policy's scope,
narrowed to the categories the provider declares: options.consentCategories
and the categories of its scripts, network rules, vendors and discovered
frames. When nothing is declared, a permissive policy shows only necessary
and a strict one its whole scope.
The necessary switch is on and disabled. Row titles and descriptions come
from consentTypes.<category>.title and .description; the action labels
from common.rejectAll, common.acceptAll and common.save. One row is
expanded at a time.
Draft and save
Switches edit a draft, not the visitor's permissions. Nothing is recorded
until Save. The draft seeds each category from the recorded choice; without
one it uses presentation.preferences.defaults from the provider options,
and without that it is on under an opt-out rule and on only for the rule's
preselected categories under an opt-in rule. Save records the displayed
categories, then reseeds the draft from the new record. Accept All and
Reject All record immediately without a separate Save. A save answers a
choice prompt, so an open choice banner closes; it does not dismiss a
notice prompt, which keeps its own acknowledgement record. Unsaved toggles
are lost when the widget unmounts.
When the policy changes in a way that affects the choice while the draft has unsaved toggles, the widget shows an alert, "The privacy policy changed. Review the current choices before saving.", with a Review choices button that resets the draft. Save is refused until then. A draft without unsaved toggles reseeds silently.
A saved grant that the current policy or a privacy signal such as Global Privacy Control overrides shows a note under its row: "Your saved choice is restricted by the current privacy settings." The switch still reflects the saved value; the effective permission is off.
Vendors
When the provider declares vendors, each category's expanded description
ends with one card per vendor whose category condition names that category,
styled like a category row: the vendor's name and a switch on the header,
and its description and privacy policy link behind the card's own expand
control. Cards start collapsed, so a long list stays one line per vendor,
and the category list scrolls inside the dialog rather than growing it.
A collapsed category or card renders its content element empty and mounts
the content the first time it opens, then keeps it mounted, so a long
vendor list costs nothing until a visitor expands its category.
Vendor switches edit the same draft as the category
switches and record on Save. Saving with one vendor off denies that vendor
only; the category stays granted and its other vendors keep loading. While
the category is off in the draft the vendor switches are disabled, with the
hint "Turn on this category to choose vendors." A vendor declared
disabled, or one that only falls under necessary, is listed without a
switch. Accept All and Reject All clear every vendor denial, including one
staged but not yet saved. Under an iab policy no vendor rows render.
Vendors that are only referenced by a script slug, without a name and
privacy policy URL, still gate loading but are not listed, and so does a
vendor whose condition negates a category, since no category row can
host it. See
vendor consent.
Actions
The preference center always renders Reject All, Accept All and Save Settings, with
Save Settings as the primary action. presentation.preferences.layout,
primaryActions, direction and uiProfile in the provider options
reorder and restyle them; a layout that omits one of the three has it
restored. Customize and dismiss actions never render here.
When consent is not owed
Without a resolved policy rule the widget renders nothing and appears as
soon as a rule resolves, without a remount. A rule with model: 'none' and
an empty rights list renders nothing; a none rule that lists any right,
such as ['disclosure'], renders the widget and Save completes without
writing a consent record.
For a fully custom preference center, useConsentDraft() returns the same
draft: values, displayedCategories, vendors, isDirty, isStale,
set, update, setVendor, acceptAll, rejectAll, save and reset.
A custom vendor control alone takes useVendorDraft(), the vendor slice of
that draft: vendors, setVendor, isDirty, isStale, save and reset.
useDeclaredVendors(), useVendorChoice() and useVendorAllowed(id) read
the declared list, the recorded denials and one vendor's effective result.
Accessibility
Each row is a disclosure: a button that expands the category description
and a switch beside it, so a visitor can read the description without
changing the choice. Each switch has the category title as its accessible
name. A category's vendor cards sit in a region named "Vendors" with the
count; each card's expand button carries aria-expanded, each vendor
switch is named "Allow" followed by the vendor name, and is described by
the vendor's name element. The policy-change alert uses role="alert", and a restriction note is
an output element linked to its switch through aria-describedby. The
root's dir attribute follows the active language.
Composition
Every part is available as ConsentWidget.<Part>. Root provides the
draft and accepts noStyle, disableAnimation and uiSource. Accordion
and AccordionItems render the stock rows; AccordionItem,
AccordionTrigger, AccordionTriggerInner, AccordionContent,
AccordionArrow and Switch build your own; VendorList renders one
category's vendor rows and takes a category prop. PolicyActions renders the
resolved actions in Footer and FooterSubGroup and accepts renderAction
to replace one button; AcceptAllButton, RejectButton, SaveButton and
CustomizeButton are the individual buttons.
Accordion is controlled: pass value and onValueChange, or no
category description ever opens. The stock ConsentWidget holds that state
for you. In this component, ConsentWidget comes from the same import as
the example at the top of the page:
Provider component slots for the stock structure are manager.root,
manager.footer, manager.actionGroup, accordion.root,
accordion.triggerRow, accordion.title, accordion.control,
accordion-item.root, accordion-item.trigger, accordion-item.content,
vendor-list.root, vendor-list.trigger, vendor-list.content,
vendor-list.item, vendor-list.header,
vendor-list.name, vendor-list.description, vendor-list.link,
vendor-list.control and tag.manager.
ConsentWidget from the package root and from the
c15t/react/consent-widget subpath loads its code in a separate chunk the
first time it renders, so a page that never renders it does not download
it. c15t/react/components/consent-widget exports the widget without that
deferral, plus each part as a named export.
Verify
Visit the page with the widget. Use a displayed optional category whose permission is
not restricted by policy or privacy signals such as GPC. One row per displayed
category appears, with necessary on and disabled. Turn that category on:
useConsent('<category>')
elsewhere on the page still reports the old value. Choose Save: it now
reports true. An open choice banner closes when the save answers its
prompt; a notice banner stays open until acknowledged. Reload the page: the
switch keeps the saved value. Without a saved choice or configured draft
defaults, optional switches start on under an opt-out rule.