Skip to main content

React Customization

Customize

Pick the right tool

ChangeUse
Brand colors, fonts, radius, spacingTheme tokens
Light and dark colors, animationDark mode and motion
Which button is filled or outlinedconsentActions
Banner shape and positionConsentBanner props or presentation
One element of a componentComponent parts
Labels and languagesCopy and translations
Different markup entirelyHeadless

Customization explains how these choices relate across frameworks.

See the design gallery for five banner designs, from a bottom bar to a fully custom one, with tested code for this framework.

Import the stylesheet yourself

A React app imports no c15t stylesheet. ConsentBanner, ConsentDialog and the other stock components render the rules they use as <style> elements.

Import the stylesheet yourself with Tailwind CSS 3, to put c15t's rules in a named cascade layer. Set styles: false in the provider options, so the components add no second copy, and import the stylesheet once in the module that renders ConsentProvider:

src/consent.tsx (partial)
import 'c15t/react/styles.css';

// In the ConsentProvider options:
styles: false,

To import the stylesheet from CSS instead, put it at the top of your global stylesheet, above any @tailwind directives. postcss-import, which Vite uses, ignores an @import that follows other rules. Tailwind 3 also needs a PostCSS plugin; Tailwind CSS shows the setup.

Change colors, fonts and radius

Colors, radii, shadows, typography, and motion come from --c15t-* custom properties. The rules the components render set the default values, as does styles.css when you import it with styles: false. To change them, render ConsentTheme with your theme where your app renders on the server, or override the variables directly in your CSS.

ConsentTheme renders a <style id="c15t-theme"> element with the variables for theme. It is not a client component: rendered from a Server Component or another server-only file, the theme generator stays out of the browser bundle. Rendered from a client component it still works, but the browser downloads the generator (about 1.7 KB gzip). The provider no longer turns theme tokens into CSS. It still reads consentActions and slot styles from its theme option, and warns in development when theme holds tokens but the page has no c15t-theme stylesheet.

ConsentTheme propPurpose
themeColors, dark colors, typography, spacing, radius, shadows and motion. Defaults to the built-in theme.
colorScheme'dark' makes the dark tokens the default before hydration. 'system' follows prefers-color-scheme with a CSS media query. Omit it to follow a dark or c15t-dark class on <html>.
nonceContent Security Policy nonce for the <style> element.

Pass the same colorScheme to ConsentTheme and to the provider. If your app manages dark mode through a root class, such as next-themes' dark class, omit colorScheme and include that class in the server HTML. Dark mode covers theme.dark, null and a dark first paint.

To switch between different token sets at runtime, render ConsentTheme from a client component and change its props, which ships the generator.

Outside a component, call generateThemeCSS(theme, colorScheme) from c15t/react/utils, or @c15t/react/utils with the scoped package. It writes the same CSS as ConsentTheme. Outside React, import it from @c15t/ui/theme. Call it on the server or at build time and put the result in a <style> element or your stylesheet. It escapes <, so the result is safe inside <style>.

A single-page app has no server render, so pick one of two ways:

  • Set the variables in your CSS. Nothing extra ships to the browser:

    src/index.css
    :root {
      --c15t-primary: #2f6f4e;
      --c15t-primary-hover: #24563c;
      --c15t-text-on-primary: #fff;
      --c15t-radius-lg: 4px;
    }
  • Render ConsentTheme next to the provider. Use this when the theme lives in TypeScript or changes at runtime. The browser downloads the theme generator.

    src/consent-theme.ts
    import { defineTheme } from 'c15t/react';
    
    export const theme = defineTheme({
      colors: { primary: '#2f6f4e', primaryHover: '#24563c' },
      radius: { lg: '4px' },
    });

    In src/consent.tsx, render <ConsentTheme theme={theme} /> from c15t/react just before <ConsentProvider>.

In a server-rendered app, such as React Router framework mode, render ConsentTheme in the root component so the <style> element is part of the server HTML. Theme tokens lists every variable.

ConsentTheme outranks a plain :root rule, wherever each one loads. If you use both, a variable set in both places takes the ConsentTheme value. Write your CSS overrides on :root:root to beat it, or move the values into the theme.

Style the action buttons

consentActions decides how each action looks. default applies to every button, primary to the action the policy or your props mark primary, and accept, reject, customize and dismiss to one action each. Pass it through the provider's theme option:

src/consent.tsx
<ConsentProvider
  options={{
    mode,
    scripts,
    theme: {
      consentActions: {
        primary: { variant: 'primary', mode: 'filled' },
        dismiss: { variant: 'neutral', mode: 'stroke' },
      },
    },
  }}
>

The provider's theme option only reads consentActions and slot styles. Tokens passed there do nothing, and the provider logs a warning in development. Changing button styles does not change the policy or expire a saved choice.

Change the banner shape

ConsentBanner takes variant (floating, bar, widget or wall) and position. To use one shape everywhere, set presentation.prompt in the provider options instead. This banner is a full-width bar for notices and a card for choices:

src/regional-banner.tsx
import { ConsentBanner, usePolicyRule } from 'c15t/react';

export function RegionalBanner() {
	const policy = usePolicyRule();
	return (
		<ConsentBanner variant={policy?.prompt === 'notice' ? 'bar' : 'floating'} />
	);
}

Render RegionalBanner in place of ConsentBanner inside the provider. ConsentBanner lists every variant and position, and how to order buttons per policy.

Style one component part

Each component part is a slot. Set slots in the provider options under components, keyed by component and part. A slot takes any attribute the element accepts, such as className or style.

The root also carries data-variant, so you can restyle one shape without touching the others. Tailwind, giving a bar a brand-colored top edge and tighter text:

<ConsentProvider
	options={{
		mode,
		presentation: { prompt: { variant: 'bar' } },
		components: {
			banner: {
				root: { className: 'group' },
				card: {
					className:
						'group-data-[variant=bar]:border-t-4 group-data-[variant=bar]:border-t-emerald-600',
				},
			},
			description: {
				banner: { className: 'group-data-[variant=bar]:text-xs' },
			},
		},
	}}
/>

data-variant sits on the root, so child slots use Tailwind's group prefix to read it.

Class names and CSS-in-JS shows CSS Modules, vanilla-extract, StyleX and Emotion classes on a part.

Set noStyle on a component to drop the built-in classes from every part and keep only what your slots pass. Set noStyle in the provider options to do it for every component.

Component parts lists every part key under components and the data-* attributes each part carries. Action buttons carry data-action, so a single rule such as [data-action='dismiss'] styles one role across every surface. Right links are underlined text by default, so the only button on a notice is the primary action.

Component parts lists every component's part keys and when to drop the built-in styles with noStyle. The ConsentGate placeholder's parts are components['consent-gate'].root, .title and .button. Class names and CSS-in-JS covers CSS Modules, vanilla-extract, StyleX and Emotion, and Tailwind CSS covers utilities on parts.

Turn on dark mode and change motion

Pass colorScheme in the provider options and the same value to ConsentTheme. Leave it unset to follow a dark class on <html>, and add dark colors with theme.dark. Dark mode covers each value and a dark first paint.

disableAnimation in the provider options turns off the banner and dialog animations, and the same prop on ConsentBanner or ConsentDialog overrides it for one surface. Motion and animation covers the duration and easing tokens and reduced motion.

Change the copy

Labels, descriptions and languages come from the translations in your Inth project and from the provider's i18n option. For the visitor's language, i18n.messages replace the project's copy key by key, and keys you leave out keep the project's wording. Copy and translations shows the message keys. For one banner, ConsentBanner also accepts text props such as dismissButtonText.

Check the result

Clear site data and reload under a policy that shows a banner. Check the banner and the preferences dialog, since both read the same tokens. Test a narrow window, keyboard focus and contrast on the primary button. In development, the console warns if theme holds tokens but the page has no c15t-theme style.