Building UI

Headless by Design

The c15t core package provides no pre-built UI components. It gives you a Zustand vanilla store with all the consent state and actions you need — you bring the rendering layer. This makes it the foundation for building consent UI in any framework: vanilla JS, Vue, Svelte, Solid, or anything else.

Vanilla DOM Example

Here's a complete consent banner built with plain JavaScript and DOM APIs:

Reactive State Subscriptions

The store fires on every state change. Compare with the previous state to react only to specific UI changes:

For analytics SDKs or consent-mode integrations, use subscribeToConsentChanges() instead of diffing consentStore.subscribe() manually:

Building Policy-Aware UI

If you use policy packs, your custom UI should render from the resolved runtime policy instead of hard-coding a fixed banner shape.

That matters when different visitors may resolve to different experiences, for example:

  • a banner in one region and no banner in another
  • different action sets such as accept/customize without reject
  • different category scopes depending on the active policy

You can preview that locally with offlinePolicy.policyPacks:

When building custom UI, prefer these runtime values:

  • activeUI to decide whether to show banner, dialog, or nothing
  • policy to inspect the resolved mode and UI hints
  • policyDecision to debug why a policy was selected
  • consentCategories to decide which categories should render

Info

See Policy Packs for offline preview setup and Policy Packs Concept for matcher precedence and fallback behavior.

Key Store Properties for UI

PropertyTypeUse
activeUI'none' | 'banner' | 'dialog'Which consent UI is currently visible
consentsRecord<string, boolean>Current consent state per category
selectedConsentsRecord<string, boolean>Staged toggle state (unsaved)
consentTypesConsentType[]Categories to display with title/description
modelstring | nullCurrent consent model (opt-in, opt-out, iab)
locationInfoLocationInfo | nullDetected location (country, region)

Key Store Actions for UI

ActionSignatureUse
saveConsents(type: 'all' | 'custom' | 'necessary') => Promise<void>Save consent choices
setSelectedConsent(name, value) => voidToggle a category (staged, not saved)
setConsent(name, value) => voidSet + auto-save a single category
setActiveUI(ui: 'none' | 'banner' | 'dialog', options?) => voidShow/hide the banner or dialog
getDisplayedConsents() => ConsentType[]Get visible consent categories

Validating Custom UI Against the Resolved Policy

If you're building a reusable headless component library or framework wrapper, test your rendered UI against the resolved policy shape.

This catches mismatches such as:

  • rendering actions the policy does not allow
  • rendering the wrong action order
  • using the wrong layout or UI profile
  • showing banner/dialog UI that does not match the resolved mode

Using @c15t/ui for Theming

The @c15t/ui package provides a framework-agnostic theme and style system. It defines design tokens, slot-based styling, and CSS primitives that you can use in any rendering layer.

Theme Tokens

Use the token types to build a consistent theme system:

Style Utilities

@c15t/ui exports DOM utilities for working with class names and styles:

When building framework adapters, treat noStyle as internal control flow only. Always sanitize resolved slot styles before binding them to DOM elements, and prefer subtree-inherited CSS variables over hard-coded :root assumptions for token-driven primitives like switches.

CSS Primitives

Import style primitives for consent UI elements:

These functions return CSS module class name generators. To apply the associated styles, import the stylesheet from your app-level CSS entrypoint:

Building a Framework Library

Info

Building a Vue, Svelte, or other framework library? Use c15t for the consent logic and @c15t/ui for the theme/style system. See the @c15t/react source code for a reference implementation of how to wrap the core store in a framework-specific API.

The pattern for building a framework wrapper:

  1. Create a provider — Initialize getOrCreateConsentRuntime() and expose the store via your framework's context system (Vue provide/inject, Svelte context, etc.)
  2. Build reactive bindings — Subscribe to store changes and bridge them to your framework's reactivity system
  3. Wrap UI components — Use @c15t/ui tokens and slots for consistent theming, render with your framework's component model
  4. Export hooks/composables — Expose store actions through framework-idiomatic APIs (Vue composables, Svelte stores, etc.)