Saltar al contenido principal

Theming

The SDK's visual chrome is driven by CSS custom properties scoped to [data-rift-root]. Consumers override at any layer — plain CSS, the typed appearance prop, or by opting in to the bundled dark token variant. This guide shows the three layers, how they compose, and what the SDK deliberately does NOT paint.

What the SDK ships

A single token sheet, loaded once when <RiftProvider> mounts. The sheet declares custom properties on [data-rift-root]:

[data-rift-root] {
--rift-color-primary: #4f46e5;
--rift-color-background: #ffffff;
--rift-color-text: #111827;
--rift-radius-md: 8px;
--rift-space-md: 16px;
--rift-font-family: ui-sans-serif, system-ui, /* … */;
/* …rest of the token set… */
}

Every SDK component class references these via var(--rift-color-…). The wrapper element itself does NOT set color, background-color, font-family, or font-size — the SDK only colours its own components, not the consumer's surrounding subtree.

What the SDK does NOT paint

Earlier the wrapper applied color, background-color, font-family, and font-size to itself. That bled SDK chrome onto every consumer who hadn't themed — children of <RiftProvider> that weren't SDK components still got SDK text colour and SDK background. As of 0.1.4 the wrapper is invisible structurally:

  • Consumer text inside <RiftProvider> keeps its host typography and colour.
  • SDK component classes still set their own colour, background, border, and font via tokens — they look themed.
  • Overriding --rift-color-primary via CSS variables works at any ancestor of the SDK subtree.

Three layers of override (in precedence order)

1. The theme prop on <RiftProvider>

Opts in to the SDK's neutral dark token variant by writing data-theme="dark" onto the wrapper:

<RiftProvider theme="dark">{children}</RiftProvider>

The dark palette is neutral, not branded — dark surface colours that work without further customization. Layer brand colors on top via appearance or CSS.

If you already drive theme state from a parent element (<html data-theme="dark">, Tailwind's .dark, etc.) the prop is optional — the dark variant matches both [data-rift-root][data-theme="dark"] AND [data-theme="dark"] [data-rift-root].

2. The typed appearance prop

JS-side typed mirror of every token. Values are written onto the wrapper as --rift-* custom properties on mount and on every change:

<RiftProvider
appearance={{
variables: {
colorPrimary: "#E61919",
colorPrimaryForeground: "#ffffff",
radiusMd: "12px",
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
},
}}
>
{children}
</RiftProvider>

CamelCase keys map 1:1 to kebab-case CSS variables: colorPrimary--rift-color-primary. Pair with theme="dark" for brand-on-dark:

<RiftProvider
theme="dark"
appearance={{ variables: { colorPrimary: "#E61919" } }}
>
{children}
</RiftProvider>

3. Plain CSS

Override any token at any DOM level. Highest specificity wins:

/* App-wide override */
[data-rift-root] {
--rift-color-primary: #ff6600;
--rift-radius-md: 4px;
}

/* Per-route override */
.checkout-page [data-rift-root] {
--rift-color-background: transparent;
}

/* Scoped to one component */
.my-sticky-cart .rift-estimate-breakdown {
--rift-font-size-md: 0.9rem;
}

CSS overrides win over the SDK's defaults via the cascade — the SDK's tokens are loaded once at the [data-rift-root] selector with the same specificity, so any consumer rule targeting a more specific selector takes precedence.

How the layers compose

In order of override precedence (highest first):

  1. Plain CSS at a more specific selector beats everything else — the consumer always has the final word.
  2. appearance.variables — inline styles on the SDK's root wrapper, beating same-selector CSS rules.
  3. Dark variant under [data-theme="dark"] (set via the theme prop OR an ancestor's attribute) — beats the light defaults.
  4. Light defaults — the fallback.

Stripe Elements pick up the SDK theme automatically

The <PaymentElement> inside <CheckoutForm> runs in a cross-origin iframe and can't read your page's CSS. The SDK derives a Stripe appearance object from its own resolved tokens (colors, radius, typography) and passes it to the Elements provider on mount — theming the SDK with appearance or the theme prop also themes the payment form. No extra configuration needed.

If you want a Stripe appearance distinct from your SDK theme, mount <Elements> yourself outside <CheckoutForm> and render the form via the useCheckout hook directly.

Token reference

TokenDefault (light)Default (dark)Purpose
--rift-color-primary#4f46e5#818cf8CTAs, links, primary indicators.
--rift-color-primary-foreground#ffffff#0a0a0aText/icons on primary surfaces.
--rift-color-background#ffffff#0a0a0aComponent backgrounds.
--rift-color-surface#f9fafb#1f2937Raised surfaces (cards, ticket rows).
--rift-color-border#e5e7eb#374151Borders and dividers.
--rift-color-text#111827#f3f4f6Primary text.
--rift-color-text-muted#6b7280#9ca3afSecondary text, captions, disclaimers.
--rift-color-error#dc2626#f87171Error states.
--rift-color-success#16a34a#4ade80Success states.
--rift-radius-sm / md / lg4/8/12 px(same)Border radius scale.
--rift-space-xsxl4/8/16/24/32 px(same)Spacing scale.
--rift-font-familysystem stack(same)Default font.
--rift-font-size-sm / md / lg0.875/1/1.125rem(same)Text scale.
--rift-font-weight-normal/semibold/bold400/600/700(same)Weight scale.

Common patterns

Branded dark theme

<RiftProvider
theme="dark"
appearance={{
variables: {
colorPrimary: "#E61919",
colorBackground: "#090A0F",
colorSurface: "#141521",
colorBorder: "rgba(255, 255, 255, 0.15)",
},
}}
>
{children}
</RiftProvider>

Theme controlled by a parent (Tailwind / next-themes / shadcn)

The SDK reacts to data-theme="dark" on any ancestor. Skip the prop entirely:

<html data-theme={isDark ? "dark" : "light"}>
<body>
<RiftProvider>
{/* SDK dark variant activates when the ancestor flips */}
</RiftProvider>
</body>
</html>

For Tailwind's .dark class strategy, alias the SDK's selector in your own CSS:

.dark [data-rift-root] {
/* re-export dark tokens here OR add data-theme="dark" to a wrapper */
}

The simpler path is to also set data-theme="dark" on the same element you put .dark on — both selectors match and the SDK's dark variant activates without extra rules.

Per-event theming

Two events on the same page can theme independently — each <RiftEvent> can sit inside its own scope:

<RiftProvider>
<RiftEvent eventId="evt_main">
<div data-theme="dark">
<AvailabilityList />
</div>
</RiftEvent>
<RiftEvent eventId="evt_after_party">
<div data-theme="light">
<AvailabilityList />
</div>
</RiftEvent>
</RiftProvider>

The data-theme attribute on the surrounding div activates the matching token variant on every [data-rift-root] descendant.

Auto-detect with prefers-color-scheme

Not built into the SDK — set the attribute from your own theme provider. The SDK reacts to whatever you write:

function MyApp({ children }) {
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
return (
<RiftProvider theme={prefersDark ? "dark" : "light"}>
{children}
</RiftProvider>
);
}