Skip to main content

TanStack Start Customization

Customize

Pick the right tool

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

ConsentRoot passes every option other than its own props through options to the React provider, so the React styling options apply unchanged. Customization explains how the choices relate.

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

Load the stylesheet yourself

The quickstart root route links no c15t stylesheet. ConsentBanner, ConsentDialog and the other stock components render the rules they use as <style> elements, in the server HTML for the banner. The rules live in @layer components, so Tailwind 4 utilities override them without extra specificity.

Load the stylesheet yourself with Tailwind CSS 3, to put c15t's rules in a named cascade layer. Set styles: false in ConsentRoot's options, so the components add no second copy, and link the stylesheet through head():

src/routes/__root.tsx (partial)
import consentCss from 'c15t/tanstack-start/styles.css?url';

// In createRootRoute({ head: () => ({ links: [...] }) }):
{ href: consentCss, rel: 'stylesheet' }

// On ConsentRoot:
options={{ styles: false }}

If your app already has a global stylesheet, you can @import the c15t stylesheet from it instead. Tailwind 3 needs a PostCSS plugin and the import above the @tailwind directives; Tailwind CSS shows the setup, and stylesheets and CSS layers explains the layer order.

Change colors, fonts and radius

Colors, radii, shadows, typography and motion come from --c15t-* CSS variables. Pick one of two ways to set them:

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

    src/styles.css
    :root {
      --c15t-primary: #2f6f4e;
      --c15t-primary-hover: #24563c;
      --c15t-text-on-primary: #fff;
      --c15t-radius-lg: 4px;
    }
  • Render ConsentTheme in the root route. Use this when the theme lives in TypeScript. The root component renders on the server, so the <style> element is in the first HTML. It also runs in the browser, so the theme generator ships in the bundle.

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

    In the root component, render <ConsentTheme theme={theme} /> from c15t/tanstack-start inside <head>, after <HeadContent />.

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.

Theme tokens lists every variable and scoping tokens to one consent model.

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 ConsentRoot's options:

src/routes/__root.tsx
<ConsentRoot
  state={consent}
  scripts={scripts}
  options={{
    theme: {
      consentActions: {
        primary: { variant: 'primary', mode: 'filled' },
      },
    },
  }}
>

options.theme only reads consentActions and slot styles. Tokens passed there do nothing, and the provider logs a warning in development.

Change the banner shape

ConsentBanner takes variant (floating, bar, widget or wall) and position, such as <ConsentBanner variant="bar" position="bottom" />. To use one shape everywhere, set options.presentation.prompt on ConsentRoot instead. ConsentBanner lists every variant and position.

Style one component part

Each component part is a slot that accepts any attribute its element takes, such as className or style. Set slots in ConsentRoot's options.components, keyed by component and part:

src/routes/__root.tsx
options={{
  components: {
    banner: {
      card: { className: 'rounded-none shadow-none' },
    },
  },
}}

Component parts lists the keys for every component 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.

Turn on dark mode and change motion

Pass colorScheme to ConsentTheme in the root route and the same value in options on ConsentRoot. 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.

options.disableAnimation on ConsentRoot 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 options.i18n on ConsentRoot. 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.

Check the result

Clear site data and reload under a policy that shows a banner. View the page source: with an awaited loader, the banner is in the HTML with your classes and, if you render ConsentTheme, a <style id="c15t-theme"> element. Open the preference dialog too, since it reads the same tokens. Test a narrow window, keyboard focus and contrast on the primary button.