FloPayFloPay
Guides

Theming

Style every FloPay component with a single `theme` prop, with `appearance` and `buttonsStyles` as targeted overrides.

Theming

FloPay ships six first-class theme bundles plus a 'classic' no-op marker. A single theme value on FloPayCheckout, FloPayAutomaticPaymentButton, or SplitCardForm paints both sides of the integration in one go:

  • the Stripe-side appearance (palette + nested-input rules)
  • the React-rendered wrapper, title, submit button, and inputs
  • on FloPayAutomaticPaymentButton, the fallback FloPayCheckout modal that opens when the saved-payment charge needs user interaction

Build a theme visually in the theme editor on our playground, then drop the resulting theme value (or copy custom appearance / buttonsStyles overrides) into your app.

Theme Bundles

theme valueThemeBundleIdAesthetic
'modern-light'Clean & airy — Inter, soft shadows, generous spacing, FloPay-blue accents
'modern-dark'Modern light with a dark surface
'bold-light'Saturated FloPay blue, gradient pill submit, heavy borders
'bold-dark'Bold light on a dark surface
'glass-light'Translucent surfaces with backdrop blur on a blue gradient
'glass-dark'Glass light on a dark surface
'classic'Marker that preserves the historic FloPay look (#EDEDFF wrapper, #4A49FF indigo submit) so consumers can declare a default explicitly

All bundles are built around FloPay blue: #1785E0 on light bundles, #60A5FA on dark bundles for contrast.

One Prop, Everywhere

import { FloPayCheckout, FloPayAutomaticPaymentButton } from '@flopay/react';

// Same look across both components
<FloPayCheckout theme="bold-dark" sessionId={sessionId} onComplete={handleSuccess} />
<FloPayAutomaticPaymentButton theme="bold-dark" sessionId={upsellSessionId} onSuccess={handleSuccess}>
  Add upsell
</FloPayAutomaticPaymentButton>

SplitCardForm accepts the same theme prop when you want to render it manually inside FloPayProvider.

Resolution Precedence

When several styling props are present, the SDK resolves them from highest priority to lowest:

  1. Explicit appearance — overrides the theme bundle's appearance (palette + Stripe rules).
  2. Explicit buttonsStyles — merges per-field on top of the theme bundle's button-layout styles.
  3. theme bundle.
  4. Legacy buttonsTheme preset (back-compat for older integrations).
  5. Hardcoded defaults.

You can drop a theme on the component and surgically override only the fields you need:

Override the submit colour on top of a theme
<FloPayCheckout
  sessionId={sessionId}
  theme="modern-light"
  buttonsStyles={{
    submitButton: { backgroundColor: '#0F172A' },
  }}
/>
Override the Stripe Elements palette on top of a theme
<FloPayCheckout
  sessionId={sessionId}
  theme="glass-dark"
  appearance={{
    variables: { colorPrimary: '#22D3EE' },
    rules: { '.Input': { border: '1px solid rgba(255,255,255,0.3)' } },
  }}
/>

Reading A Bundle From @flopay/shared

The bundle map is exported so you can render swatches, preview cards, or merge an appearance into another system without re-implementing the look:

import {
  THEMES,
  resolveTheme,
  MODERN_LIGHT_APPEARANCE,
  BUTTONS_LAYOUT_MODERN_LIGHT,
} from '@flopay/shared';

const glassDark = THEMES['glass-dark'];
// { appearance: FloPayAppearance, buttonsLayout: ButtonsLayoutStyles }

const maybeBundle = resolveTheme('bold-light');
if (maybeBundle) {
  console.log(maybeBundle.appearance.variables?.colorPrimary);
}

// Direct constant access — handy when you want to merge two themes
const merged = {
  ...MODERN_LIGHT_APPEARANCE,
  variables: { ...MODERN_LIGHT_APPEARANCE.variables, colorPrimary: '#FF7A1A' },
};

resolveTheme('classic') returns undefined'classic' is a marker, not a bundle, so the SDK falls through to its hardcoded defaults.

Runtime Theme Swapping

StripeAdapter.getElements() detects appearance changes between calls and live-updates the cached Stripe Elements group via elements.update({ appearance }). That makes runtime theme swaps actually re-style mounted card iframes:

const [mode, setMode] = useState<'light' | 'dark'>('light');

<FloPayCheckout
  sessionId={sessionId}
  theme={mode === 'dark' ? 'modern-dark' : 'modern-light'}
  onComplete={handleSuccess}
/>

Previously the Stripe iframe kept its stale styling because the elements group was cached. With the new adapter behaviour, swapping theme (or appearance) at runtime takes effect on mounted inputs.

Appearance Derivation

When you pass appearance without a matching buttonsStyles, SplitCardForm now derives its React-side styling from appearance.variables:

  • wrapper background falls back to appearance.variables.colorBackground (the SDK default #FFFFFF is skipped so the historic #EDEDFF tint is preserved for 'classic')
  • submit-button background falls back to appearance.colorPrimary
  • title colour, input colour, border radius, and font family all derive from appearance.variables

In practice, passing a custom appearance alone now produces a visibly themed form across layout="default" and layout="buttons".

Theme Variables Reference

VariableTypeDescription
colorPrimaryCSS colorButtons, links, focus rings
colorBackgroundCSS colorElement backgrounds
colorTextCSS colorLabels and input text
colorDangerCSS colorError messages and invalid states
borderRadiusCSS lengthCorner rounding for inputs and buttons
fontFamilyCSS font-familyFont stack for all elements
fontSizeBaseCSS lengthBase font size
spacingUnitCSS lengthBase spacing multiplier

appearance.rules accepts CSS-like blocks keyed by Stripe Element selectors (.Input, .Input:focus, .Label, .Tab, .Tab--selected) for fine-grained styling inside the iframe.

Buttons Layout Slots

When you use FloPayCheckout with layout="buttons", the expanded card form supports content slots for the card button, back button label, and title:

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  cardButtonContent="Pay by card"
  cardBackButtonContent=""
  cardTitleContent="Enter card details"
  onComplete={handleSuccess}
/>

Pass any ReactNode to these props. Passing '' removes the text completely. The demo /theme playground includes controls for these slot props alongside theme and buttonsStyles.

Migrating From buttonsTheme

Legacy values still resolve via resolveButtonsLayoutTheme() for back-compat. Older integrations keep working untouched — the prop is silently ignored if theme is also supplied — but new code should use theme:

OldNew
buttonsTheme="default"theme="classic"
buttonsTheme="minimal"theme="modern-light"
buttonsTheme="rounded"theme="modern-light" or theme="bold-light"
buttonsTheme="dark"theme="bold-dark"

See Also

On this page