Theming
Override accent colour, fonts, surfaces, radius, and spacing via CSS variables. Two levels of customization.
Every visual value in @crxpay/react-paywall is a CSS variable on a single
root selector ([data-crxpay-root]). The provider sets defaults; you override
them. There's no Tailwind dependency, no CSS-in-JS runtime, no class-name
escape-hatch — just variables.
The 90% case: pass a theme prop
<CrxPayProvider
apiKey="…"
theme={{ accent: '#10B981' }}
>
…
</CrxPayProvider>
That single override re-themes every CTA, selected-plan border, and badge.
The borderActive token defaults to accent automatically, so the selected-
plan ring follows.
All available tokens
interface BrandTheme {
// Colors
accent: string; // Primary CTA + accent
accentForeground: string; // Text on top of accent (usually white)
text: string; // Primary copy
textMuted: string; // Body / supporting copy
textFaint: string; // Hints / footers
surface: string; // Card backgrounds
surfaceMuted: string; // Subtle surfaces / hover
border: string; // Default border
borderActive: string; // Selected-state border (defaults to accent)
success: string; // Savings %, paid status
danger: string; // Errors, expiry warnings
backdrop: string; // Modal backdrop with alpha
// Typography
fontHeading: string; // Headings
fontBody: string; // Body copy
// Optional `fontDisplay` for display-style headings (used by templates)
// Geometry
radius: string; // Default border-radius (e.g. '12px')
}
Dark mode
Pass a full theme with dark surfaces:
const darkTheme = {
accent: '#A78BFA',
accentForeground: '#FFFFFF',
text: '#F4F4F8',
textMuted: '#C7C7D1',
textFaint: '#8B8B98',
surface: '#1A1A2E',
surfaceMuted: '#23233F',
border: '#2D2D4F',
backdrop: 'rgba(0,0,0,0.7)',
};
<CrxPayProvider apiKey="…" theme={darkTheme}>…</CrxPayProvider>
Or scope it via media query in your own CSS:
@media (prefers-color-scheme: dark) {
[data-crxpay-root] {
--crxpay-text: #F4F4F8;
--crxpay-surface: #1A1A2E;
/* …rest of dark tokens… */
}
}
The 100% case: override CSS vars directly
Every token in BrandTheme maps to a CSS variable named --crxpay-{kebab}.
Override anything the prop doesn't expose:
[data-crxpay-root] {
--crxpay-radius: 20px; /* softer cards */
--crxpay-font-heading: 'Inter Display', sans-serif;
--crxpay-backdrop: rgba(0,0,0,0.85); /* darker modal scrim */
}
These cascade — a data-crxpay-root deeper in the tree (e.g. wrapping a
nested provider) will override outer values for its subtree only.
Custom fonts
Two approaches.
1. Apply globally in your app
Import the font once at app entry:
import '@fontsource/inter';
import '@fontsource/space-mono';
Then point the theme at it:
<CrxPayProvider
apiKey="…"
theme={{
fontHeading: '"Space Mono", monospace',
fontBody: '"Inter", sans-serif',
}}
>
2. Scope to crxpay only
@font-face {
font-family: 'PixelFont';
src: url('/fonts/pixel.woff2') format('woff2');
}
[data-crxpay-root] {
--crxpay-font-display: 'PixelFont', monospace;
}
(Templates that use a display font opt into it via useDisplayFont on the
hero block — see the API reference.)
Theming nested paywalls
Mounting a second <CrxPayProvider> inside the first creates a second
data-crxpay-root wrapper. Inner CSS variables shadow outer ones, so you
can render two paywalls side-by-side with totally different brands — useful
for the marketing-page comparison demos.
Reduced motion
Animations honour prefers-reduced-motion: reduce. <PaywallModal> collapses
to opacity-only transitions; <TrialBadge> skeleton shimmer is disabled.
Nothing for you to wire — it's automatic.
Class escape hatch
Components accept an optional className you can use to add additional
selectors. Avoid it where possible — the CSS variables cover most cases — but
useful for layout adjustments:
<UpgradeButton className="my-popup__cta">
Upgrade
</UpgradeButton>
Then in your stylesheet:
.my-popup__cta {
margin-top: 24px;
width: 100%;
}
What you can't override (yet)
- Internal animation easings
- Modal portal target (use
portalContainerprop instead) - Block-level layout structure (paywall blocks themselves are themable but not restructurable)
These are intentional v1 constraints. If you hit one and want it lifted, open an issue with the use-case.
Was this page helpful?
Your feedback shapes what we document next.