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-primaryvia 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):
- Plain CSS at a more specific selector beats everything else — the consumer always has the final word.
appearance.variables— inline styles on the SDK's root wrapper, beating same-selector CSS rules.- Dark variant under
[data-theme="dark"](set via thethemeprop OR an ancestor's attribute) — beats the light defaults. - 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
| Token | Default (light) | Default (dark) | Purpose |
|---|---|---|---|
--rift-color-primary | #4f46e5 | #818cf8 | CTAs, links, primary indicators. |
--rift-color-primary-foreground | #ffffff | #0a0a0a | Text/icons on primary surfaces. |
--rift-color-background | #ffffff | #0a0a0a | Component backgrounds. |
--rift-color-surface | #f9fafb | #1f2937 | Raised surfaces (cards, ticket rows). |
--rift-color-border | #e5e7eb | #374151 | Borders and dividers. |
--rift-color-text | #111827 | #f3f4f6 | Primary text. |
--rift-color-text-muted | #6b7280 | #9ca3af | Secondary text, captions, disclaimers. |
--rift-color-error | #dc2626 | #f87171 | Error states. |
--rift-color-success | #16a34a | #4ade80 | Success states. |
--rift-radius-sm / md / lg | 4/8/12 px | (same) | Border radius scale. |
--rift-space-xs … xl | 4/8/16/24/32 px | (same) | Spacing scale. |
--rift-font-family | system stack | (same) | Default font. |
--rift-font-size-sm / md / lg | 0.875/1/1.125rem | (same) | Text scale. |
--rift-font-weight-normal/semibold/bold | 400/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>
);
}
Related
<RiftProvider>—appearance— typed prop reference.<RiftProvider>—theme— opt-in dark variant.