Custom themes
Create a theme from scratch that matches your brand
This guide walks through creating a complete custom theme. We'll build an "Ocean" theme with cool blues and clean typography.
Before you start
Gather your brand assets:
- Primary color (your main brand color)
- Font choices (or stick with system fonts)
- Logo (optional, for start screen)
Step 1: Create the theme file
Create src/theme/themes/ocean.ts:
import { ThemeConfig } from '../types';
export const oceanTheme: ThemeConfig = {
// We'll fill this in step by step
};Step 2: Add metadata
Every theme needs an ID, name, and optional description:
export const oceanTheme: ThemeConfig = {
id: 'ocean',
name: 'Ocean',
description: 'Cool blues with clean typography',
// More sections coming...
};Step 3: Define colors first
Start by defining your color palette. Use CSS-in-JS color values (hex or rgba):
// Color constants (for easy reuse)
const ACCENT = '#0284C7'; // Primary blue
const ACCENT_LIGHT = '#E0F2FE'; // Light blue background
const ACCENT_DARK = '#0369A1'; // Darker blue for hover
const TEXT_PRIMARY = '#0C4A6E'; // Dark blue-gray text
const TEXT_SECONDARY = '#64748B'; // Medium gray
const BORDER = '#BAE6FD'; // Light blue border
const BACKGROUND = '#F0F9FF'; // Very light blue backgroundDefine color constants at the top of your file. This makes it easy to maintain consistency and adjust the palette later.
Step 4: Configure each section
Now fill in each theme section. Here's the complete ocean theme:
Header
header: {
backgroundColor: ACCENT_LIGHT,
iconColor: ACCENT,
textColor: TEXT_PRIMARY,
timeFontSize: '14px',
timeFontWeight: '600',
progressBar: {
backgroundColor: 'rgba(7, 89, 133, 0.15)',
highlightColor: ACCENT,
},
},Main content
mainContent: {
backgroundColor: BACKGROUND,
},Cards
cards: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
borderColor: BORDER,
borderRadius: '16px',
shadow: '0 4px 20px rgba(2, 132, 199, 0.1)',
titleFontSize: '18px',
titleFontWeight: '600',
durationBadgeFontSize: '14px',
numberFontSize: '14px',
numberFontWeight: '600',
image: {
placeholderColor: ACCENT_LIGHT,
durationBadgeBackground: 'rgba(12, 74, 110, 0.7)',
durationBadgeText: BACKGROUND,
},
},Step indicators
stepIndicators: {
active: {
outlineColor: ACCENT,
numberColor: ACCENT,
backgroundColor: BACKGROUND,
},
inactive: {
borderColor: BORDER,
numberColor: TEXT_SECONDARY,
backgroundColor: BACKGROUND,
},
completed: {
backgroundColor: ACCENT,
checkmarkColor: '#FFFFFF',
},
},Buttons
buttons: {
primary: {
backgroundColor: ACCENT,
textColor: '#FFFFFF',
hoverBackground: ACCENT_DARK,
iconColor: '#FFFFFF',
fontSize: '18px',
fontWeight: '600',
},
secondary: {
backgroundColor: BACKGROUND,
textColor: TEXT_PRIMARY,
borderColor: BORDER,
hoverBackground: ACCENT_LIGHT,
fontSize: '16px',
fontWeight: '500',
},
download: {
backgroundColor: 'transparent',
textColor: ACCENT,
borderColor: ACCENT,
hoverBackground: 'rgba(2, 132, 199, 0.1)',
iconColor: ACCENT,
fontSize: '18px',
fontWeight: '600',
},
transcription: {
backgroundColor: '#FFFFFF',
iconColor: ACCENT,
hoverBackground: BACKGROUND,
},
},Typography
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Inter', 'sans-serif'],
numbers: ['Inter'],
},
},Branding
branding: {
logoUrl: undefined, // Or your logo URL
showLogoBorder: true,
logoSize: 'fit',
},Mini player
miniPlayer: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
titleFontSize: '16px',
titleFontWeight: '500',
transcriptionFontSize: '15px',
progressBar: {
backgroundColor: ACCENT_LIGHT,
highlightColor: ACCENT_DARK,
},
controls: {
playButtonBackground: ACCENT,
playButtonIcon: '#FFFFFF',
otherButtonsBackground: ACCENT_LIGHT,
otherButtonsIcon: '#075985',
},
minimized: {
playButtonIcon: ACCENT,
},
},Sheets (bottom sheets)
sheets: {
backgroundColor: '#FFFFFF',
handleColor: BORDER,
textColor: TEXT_PRIMARY,
borderColor: ACCENT_LIGHT,
titleFontSize: '18px',
titleFontWeight: '700',
},Status colors
status: {
success: ACCENT,
error: '#DC2626',
warning: '#F59E0B',
},Loading states
loading: {
spinnerColor: ACCENT,
backgroundColor: BACKGROUND,
messageFontSize: '16px',
messageFontWeight: '500',
},Start card
startCard: {
titleFontSize: '30px',
titleFontWeight: '700',
titleLineHeight: '1.2',
metaFontSize: '14px',
metaFontWeight: '400',
metaColor: TEXT_SECONDARY,
descriptionFontSize: '16px',
offlineMessage: {
backgroundColor: 'rgba(2, 132, 199, 0.08)',
borderColor: 'rgba(2, 132, 199, 0.25)',
textColor: ACCENT,
},
},Inputs
inputs: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
borderColor: BORDER,
focusBorderColor: ACCENT,
placeholderColor: '#94A3B8',
},Semantic colors
colors: {
text: {
primary: TEXT_PRIMARY,
secondary: '#475569',
tertiary: TEXT_SECONDARY,
inverse: '#FFFFFF',
},
border: {
light: BACKGROUND,
medium: ACCENT_LIGHT,
dark: BORDER,
},
background: {
primary: '#FFFFFF',
secondary: BACKGROUND,
tertiary: ACCENT_LIGHT,
},
},Step 5: Register the theme
Add your theme to src/theme/themes/index.ts:
import { defaultLightTheme } from './default-light';
import { defaultDarkTheme } from './default-dark';
import { oceanTheme } from './ocean';
export const themes = {
'default-light': defaultLightTheme,
'default-dark': defaultDarkTheme,
'ocean': oceanTheme,
};
export type ThemeName = keyof typeof themes;Step 6: Load custom fonts (if needed)
If you're using fonts not already loaded, add them to index.html:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">Step 7: Test your theme
- Set
"themeId": "ocean"in yourmetadata.json - Run
bun run dev - Check every screen:
- Start screen
- Stop cards
- Mini player (expanded and minimized)
- Language sheet
- Rating sheet
- Loading states
- Error states
Step 8: Check contrast
Use a contrast checker to verify readability:
- WebAIM Contrast Checker
- Aim for 4.5:1 ratio for body text (WCAG AA)
- Aim for 7:1 for enhanced accessibility (WCAG AAA)
Low-contrast themes look modern but are hard to read for many visitors. Prioritize legibility over aesthetics.
Complete example
Here's the full ocean theme file for reference:
Complete ocean.ts
import { ThemeConfig } from '../types';
const ACCENT = '#0284C7';
const ACCENT_LIGHT = '#E0F2FE';
const ACCENT_DARK = '#0369A1';
const TEXT_PRIMARY = '#0C4A6E';
const TEXT_SECONDARY = '#64748B';
const BORDER = '#BAE6FD';
const BACKGROUND = '#F0F9FF';
export const oceanTheme: ThemeConfig = {
id: 'ocean',
name: 'Ocean',
description: 'Cool blues with clean typography',
header: {
backgroundColor: ACCENT_LIGHT,
iconColor: ACCENT,
textColor: TEXT_PRIMARY,
timeFontSize: '14px',
timeFontWeight: '600',
progressBar: {
backgroundColor: 'rgba(7, 89, 133, 0.15)',
highlightColor: ACCENT,
},
},
mainContent: {
backgroundColor: BACKGROUND,
},
cards: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
borderColor: BORDER,
borderRadius: '16px',
shadow: '0 4px 20px rgba(2, 132, 199, 0.1)',
titleFontSize: '18px',
titleFontWeight: '600',
durationBadgeFontSize: '14px',
numberFontSize: '14px',
numberFontWeight: '600',
image: {
placeholderColor: ACCENT_LIGHT,
durationBadgeBackground: 'rgba(12, 74, 110, 0.7)',
durationBadgeText: BACKGROUND,
},
},
stepIndicators: {
active: {
outlineColor: ACCENT,
numberColor: ACCENT,
backgroundColor: BACKGROUND,
},
inactive: {
borderColor: BORDER,
numberColor: TEXT_SECONDARY,
backgroundColor: BACKGROUND,
},
completed: {
backgroundColor: ACCENT,
checkmarkColor: '#FFFFFF',
},
},
buttons: {
primary: {
backgroundColor: ACCENT,
textColor: '#FFFFFF',
hoverBackground: ACCENT_DARK,
iconColor: '#FFFFFF',
fontSize: '18px',
fontWeight: '600',
},
secondary: {
backgroundColor: BACKGROUND,
textColor: TEXT_PRIMARY,
borderColor: BORDER,
hoverBackground: ACCENT_LIGHT,
fontSize: '16px',
fontWeight: '500',
},
download: {
backgroundColor: 'transparent',
textColor: ACCENT,
borderColor: ACCENT,
hoverBackground: 'rgba(2, 132, 199, 0.1)',
iconColor: ACCENT,
fontSize: '18px',
fontWeight: '600',
},
transcription: {
backgroundColor: '#FFFFFF',
iconColor: ACCENT,
hoverBackground: BACKGROUND,
},
},
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Inter', 'sans-serif'],
numbers: ['Inter'],
},
},
branding: {
logoUrl: undefined,
showLogoBorder: true,
logoSize: 'fit',
},
miniPlayer: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
titleFontSize: '16px',
titleFontWeight: '500',
transcriptionFontSize: '15px',
progressBar: {
backgroundColor: ACCENT_LIGHT,
highlightColor: ACCENT_DARK,
},
controls: {
playButtonBackground: ACCENT,
playButtonIcon: '#FFFFFF',
otherButtonsBackground: ACCENT_LIGHT,
otherButtonsIcon: '#075985',
},
minimized: {
playButtonIcon: ACCENT,
},
},
sheets: {
backgroundColor: '#FFFFFF',
handleColor: BORDER,
textColor: TEXT_PRIMARY,
borderColor: ACCENT_LIGHT,
titleFontSize: '18px',
titleFontWeight: '700',
},
status: {
success: ACCENT,
error: '#DC2626',
warning: '#F59E0B',
},
loading: {
spinnerColor: ACCENT,
backgroundColor: BACKGROUND,
messageFontSize: '16px',
messageFontWeight: '500',
},
startCard: {
titleFontSize: '30px',
titleFontWeight: '700',
titleLineHeight: '1.2',
metaFontSize: '14px',
metaFontWeight: '400',
metaColor: TEXT_SECONDARY,
descriptionFontSize: '16px',
offlineMessage: {
backgroundColor: 'rgba(2, 132, 199, 0.08)',
borderColor: 'rgba(2, 132, 199, 0.25)',
textColor: ACCENT,
},
},
inputs: {
backgroundColor: '#FFFFFF',
textColor: TEXT_PRIMARY,
borderColor: BORDER,
focusBorderColor: ACCENT,
placeholderColor: '#94A3B8',
},
colors: {
text: {
primary: TEXT_PRIMARY,
secondary: '#475569',
tertiary: TEXT_SECONDARY,
inverse: '#FFFFFF',
},
border: {
light: BACKGROUND,
medium: ACCENT_LIGHT,
dark: BORDER,
},
background: {
primary: '#FFFFFF',
secondary: BACKGROUND,
tertiary: ACCENT_LIGHT,
},
},
};