frame-svg has a built-in theme variable system. Variables are named colors that resolve to different values in dark and light mode.
The output SVG embeds both themes in a single file. Wherever you embed it — a GitHub README, a docs site, any webpage — it automatically matches the user's dark or light preference via
prefers-color-scheme. No JavaScript, no duplicated files.
Prefix any color prop with $ to use a variable:
<Container background="$surface" border={{ width: 1, color: '$subtle' }}>
<Text color="$text">Hello</Text>
<Text color="$muted">Secondary</Text>
</Container>| Variable | Dark | Light | Use for |
|---|---|---|---|
$bg |
#0f172a |
#ffffff |
Page background |
$surface |
#1e293b |
#f1f5f9 |
Cards, panels |
$raised |
#334155 |
#e2e8f0 |
Elevated elements |
$subtle |
#475569 |
#cbd5e1 |
Borders, dividers |
$text |
#f8fafc |
#0f172a |
Primary text |
$muted |
#94a3b8 |
#64748b |
Secondary text |
$faint |
#475569 |
#94a3b8 |
Labels, captions |
$accent |
#38bdf8 |
#0284c7 |
Brand, interactive |
$success |
#4ade80 |
#16a34a |
Positive states |
$warning |
#fbbf24 |
#d97706 |
Caution states |
$danger |
#f87171 |
#dc2626 |
Error states |
$accentBg |
#0c4a6e |
#e0f2fe |
Accent backgrounds |
$successBg |
#166534 |
#dcfce7 |
Success backgrounds |
$warningBg |
#78350f |
#fef3c7 |
Warning backgrounds |
$dangerBg |
#7f1d1d |
#fee2e2 |
Danger backgrounds |
The dev server preview has a toggle button (top right) to simulate both themes as you design.
All theme configuration lives in themes/theme.ts. Edit it directly — the theme object is passed as-is to Template:
// themes/theme.ts
export const theme: RenderOptions = {
variables: {
accent: { dark: '#a78bfa', light: '#7c3aed' },
bg: { dark: '#0a0a0f', light: '#fafafa' },
// only the variables you specify are overridden — the rest use the defaults
},
}// src/main.frame
import { theme } from '../themes/theme.ts'
<Template theme={theme}>
<Container padding={40} background="$bg">
{/* your content */}
</Container>
</Template>Add font entries to theme.fonts in themes/theme.ts. The renderer embeds each font as base64 inside the SVG — no external requests.
// themes/theme.ts
export const theme: RenderOptions = {
fonts: [
{ family: 'Inter', src: './assets/fonts/Inter-Regular.woff2', weight: 400 },
{ family: 'Inter', src: './assets/fonts/Inter-Bold.woff2', weight: 700 },
],
variables: { /* ... */ },
}Then use fontFamily on any Text:
<Text fontFamily="Inter" fontWeight="700" fontSize={24} color="$text">Custom font</Text>| Field | Type | Description |
|---|---|---|
family |
string |
Font family name (matches fontFamily prop on Text) |
src |
string |
Path to the font file, relative to the project root |
weight |
number | string |
Font weight, e.g. 400, 700 |
style |
'normal' | 'italic' |
Font style (default 'normal') |
Place font files in assets/fonts/ at the project root, alongside src/:
frame-svg/
├── assets/
│ └── fonts/
│ ├── Inter-Regular.woff2
│ └── Inter-Bold.woff2
├── src/
│ └── main.frame
└── dist/
theme.animation in themes/theme.ts configures timing defaults and named presets used by the animate prop:
// themes/theme.ts
export const theme: RenderOptions = {
animation: {
duration: { fast: '150ms', base: '300ms', slow: '600ms' },
easing: { default: 'ease-out', bounce: 'cubic-bezier(0.34,1.56,0.64,1)' },
presets: {
pop: {
keyframes: { '0%': { transform: 'scale(0.8)', opacity: 0 }, '100%': { transform: 'scale(1)', opacity: 1 } },
duration: '250ms',
easing: 'cubic-bezier(0.34,1.56,0.64,1)',
},
},
},
variables: { /* ... */ },
}See Animations for the full animate API.
You can always pass a raw hex/rgba value instead of a variable:
<Text color="#ff0000" />
<Container background="rgba(0,0,0,0.5)" />Raw colors are static and don't respond to theme changes.