Theming
v3 introduces a design token system that lets you apply consistent visual styles across all elements from a single configuration object. Tokens control colors, typography, spacing, and borders — all from a single theme object passed at initialization.
How theming works
Theme tokens are passed to BasisTheory() at initialization and injected into every element iframe as CSS variables. Elements use those variables for their appearance.
BasisTheory(key, { theme, darkTheme, themeMode })
└─ injected as CSS variables into all element iframes
Basic setup
import BasisTheory from '@basis-theory/web-elements';
const bt = BasisTheory('<PUBLIC_API_KEY>', {
themeMode: 'auto', // 'light' | 'dark' | 'auto' — defaults to 'auto'
theme: {
colors: {
primary: '#6366f1',
error: '#ef4444',
success: '#22c55e',
text: {
primary: '#111827',
secondary: '#6b7280',
placeholder: '#9ca3af',
disabled: '#d1d5db',
},
background: {
default: '#ffffff',
hover: '#f9fafb',
focus: '#f5f3ff',
disabled: '#f3f4f6',
},
border: {
default: '#d1d5db',
hover: '#a5b4fc',
focus: '#6366f1',
error: '#ef4444',
},
},
typography: {
fontFamily: '"Inter", sans-serif',
fontSize: {
sm: '14px',
base: '16px',
lg: '18px',
},
fontWeight: {
normal: '400',
medium: '500',
semibold: '600',
},
},
spacing: {
xs: '4px',
sm: '8px',
md: '12px',
lg: '16px',
xl: '24px',
},
borders: {
radius: {
none: '0',
sm: '2px',
base: '6px',
lg: '12px',
full: '9999px',
},
width: {
thin: '1px',
base: '1.5px',
thick: '2px',
},
},
},
});
Dark mode
Pass a darkTheme object to provide a separate set of tokens for dark mode. When themeMode is 'auto', the SDK applies theme or darkTheme based on the OS prefers-color-scheme media query.
const bt = BasisTheory('<PUBLIC_API_KEY>', {
themeMode: 'auto',
theme: {
colors: {
primary: '#6366f1',
text: { primary: '#111827' },
background: { default: '#ffffff' },
border: { default: '#d1d5db', focus: '#6366f1' },
error: '#ef4444',
success: '#22c55e',
},
typography: { fontFamily: 'Inter, sans-serif', fontSize: { base: '16px' }, fontWeight: { normal: '400' } },
spacing: { sm: '8px', md: '12px', lg: '16px' },
borders: { radius: { base: '6px' }, width: { base: '1.5px' } },
},
darkTheme: {
colors: {
primary: '#818cf8',
text: { primary: '#f9fafb' },
background: { default: '#1f2937' },
border: { default: '#374151', focus: '#818cf8' },
error: '#f87171',
success: '#4ade80',
},
typography: { fontFamily: 'Inter, sans-serif', fontSize: { base: '16px' }, fontWeight: { normal: '400' } },
spacing: { sm: '8px', md: '12px', lg: '16px' },
borders: { radius: { base: '6px' }, width: { base: '1.5px' } },
},
});
If darkTheme is omitted and themeMode is 'auto' or 'dark', elements fall back to using the light theme tokens.
Switching theme mode at runtime
Call bt.updateThemeMode() to switch all mounted elements to a new mode without remounting:
// Toggle between light and dark
const current = getCurrentMode(); // your own state
await bt.updateThemeMode(current === 'light' ? 'dark' : 'light');
To update a single element's theme mode without affecting others, use element.update():
await cardNumberEl.update({ themeMode: 'dark' });
Design token reference
All tokens have built-in defaults — you only need to provide the tokens you want to override.
colors
| Token | Type | Description |
|---|---|---|
colors.primary | string | Primary accent color (focus rings, active states) |
colors.error | string | Error state color |
colors.success | string | Success / complete state color |
colors.text.primary | string | Main input text color |
colors.text.secondary | string | Secondary text color |
colors.text.placeholder | string | Placeholder text color |
colors.text.disabled | string | Text color when element is disabled |
colors.background.default | string | Default background |
colors.background.hover | string | Background on hover |
colors.background.focus | string | Background when focused |
colors.background.disabled | string | Background when disabled |
colors.border.default | string | Default border color |
colors.border.hover | string | Border color on hover |
colors.border.focus | string | Border color when focused |
colors.border.error | string | Border color in error state |
typography
| Token | Type | Description |
|---|---|---|
typography.fontFamily | string | Font stack |
typography.fontSize.sm | string | Small font size |
typography.fontSize.base | string | Default font size |
typography.fontSize.lg | string | Large font size |
typography.fontWeight.normal | string | Normal weight |
typography.fontWeight.medium | string | Medium weight |
typography.fontWeight.semibold | string | Semibold weight |
typography.lineHeight.tight | string | Tight line height |
typography.lineHeight.normal | string | Normal line height |
typography.lineHeight.relaxed | string | Relaxed line height |
spacing
| Token | Description |
|---|---|
spacing.xs | Extra-small spacing |
spacing.sm | Small spacing |
spacing.md | Medium spacing |
spacing.lg | Large spacing |
spacing.xl | Extra-large spacing |
borders
| Token | Description |
|---|---|
borders.radius.none | No radius ('0') |
borders.radius.sm | Small radius |
borders.radius.base | Default radius |
borders.radius.md | Medium radius |
borders.radius.lg | Large radius |
borders.radius.full | Full / pill radius |
borders.width.thin | Thin border width |
borders.width.base | Default border width |
borders.width.thick | Thick border width |
shadows (optional)
shadows: {
none: 'none',
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
focus: '0 0 0 3px rgba(59, 130, 246, 0.1)',
}
transitions (optional)
transitions: {
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
base: '200ms cubic-bezier(0.4, 0, 0.2, 1)',
slow: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
}