crxpaydocs

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 portalContainer prop 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.