Theming
Every component accepts a themeOptions prop of type CustomThemeOptions. The same object shape works across every component, so a single theme can style your entire integration.
You can apply themes in two ways:
- Per component — pass
themeOptionsdirectly. Useful when different widgets need different looks. - Globally — wrap your app in
ThemeProviderand omitthemeOptionsper component.
Top-level options
interface CustomThemeOptions {
mode?: "light" | "dark";
colorSchemes?: {
light?: CustomColors;
dark?: CustomColors;
};
customBreakpoints?: CustomBreakpoints;
spacingScale?: CustomSpacingScale;
customFontFamily?: {
light?: FontFamilyConfig;
dark?: FontFamilyConfig;
};
customRadius?: {
light?: CustomBorderRadius;
dark?: CustomBorderRadius;
};
border?: {
light?: CustomBorderSize;
dark?: CustomBorderSize;
};
imageBackgroundGradient?: ImageBackgroundGradient;
}Mode
"light" (default) or "dark". Components automatically pull color and font tokens from the active mode.
<ClassicQuizPlay {...otherProps} themeOptions={{ mode: "dark" }} />Color schemes
Define semantic palette tokens per mode. The shape is identical for light and dark:
const themeOptions: CustomThemeOptions = {
mode: "light",
colorSchemes: {
light: {
palette: {
primary: {
plainColor: "#1A77D2",
outlinedBorder: "#1A77D2",
onPrimary: "#FAFAFA",
primaryContainer: "#2397F3",
},
success: { plainColor: "#4CAF50", outlinedBorder: "#4CAF50", softBg: "#E3FBE3" },
danger: { plainColor: "#F44336", outlinedBorder: "#F44336", softBg: "#FEE4E2" },
warning: { plainColor: "#DC6803", softBg: "#FEF0C7" },
},
textPrimary: "#212121",
textSecondary: "#212121",
textBody: "#424242",
textColor: "#212121",
textDisabled: "#212121",
surface: "#FFFFFF",
onSurface: "#F5F5F5",
surfaceVariant: "#EEEEEE",
surfaceTintDim: "#212121",
surfaceInverse: "#F5F5F5",
outlineEnabledBorder: "#E0E0E0",
secondaryContainer: "#BDBDBD",
},
},
};Palette tokens
| Token | Used for |
|---|---|
primary.plainColor | Primary buttons, accent strokes |
primary.primaryContainer | Filled containers and highlighted backgrounds |
primary.onPrimary | Text/icon color on primary.plainColor |
primary.outlinedBorder | Outlined-button borders |
success.* / danger.* / warning.* | Status colors for correct/incorrect/warning states |
Surface and text tokens
| Token | Used for |
|---|---|
surface | Main card background |
onSurface | Secondary surface (input fields, footer) |
surfaceVariant | Tertiary background (badges, disabled rows) |
surfaceInverse | Inverted surface (overlays, tooltips) |
surfaceTintDim | Subtle tint over surfaces |
outlineEnabledBorder | Default border color |
textPrimary / textSecondary / textBody / textColor / textDisabled | Text colors at different emphases |
Typography
Provide a primary and secondary font family per mode:
const themeOptions: CustomThemeOptions = {
customFontFamily: {
light: {
primary: "Inter, sans-serif",
secondary: "Roboto, sans-serif",
},
dark: {
primary: "Inter, sans-serif",
secondary: "Roboto, sans-serif",
},
},
};Web fonts with FontConfig
To pull a web font automatically (e.g., from Google Fonts), use a FontConfig object instead of a string:
import { FontConfig } from "fansunited-frontend-core";
const customFont: FontConfig = {
family: "Inter",
url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
weights: [400, 500, 600, 700],
display: "swap",
};
const themeOptions: CustomThemeOptions = {
customFontFamily: {
light: { primary: customFont, secondary: "Roboto, sans-serif" },
dark: { primary: customFont, secondary: "Roboto, sans-serif" },
},
};The component injects the @import URL into its shadow root so the font is available regardless of host-page CSS.
Spacing scale
Override the spacing scale used for paddings, margins, and gaps:
const themeOptions: CustomThemeOptions = {
spacingScale: {
"3xs": "2px",
"2xs": "4px",
xs: "8px",
sm: "12px",
md: "16px",
lg: "24px",
xl: "32px",
"2xl": "40px",
"3xl": "48px",
},
};Corner radius
Per-mode radius tokens:
const themeOptions: CustomThemeOptions = {
customRadius: {
light: {
none: "0px",
"2xs": "2px",
xs: "4px",
sm: "8px",
md: "12px",
lg: "16px",
xl: "24px",
"2xl": "32px",
full: "1000px",
},
dark: {
// …same structure
},
},
};Border size
const themeOptions: CustomThemeOptions = {
border: {
light: { size: "1px" },
dark: { size: "2px" },
},
};Breakpoints
Override the responsive breakpoints used inside the components:
const themeOptions: CustomThemeOptions = {
customBreakpoints: {
values: {
xs: 0,
sm: 444,
md: 600,
lg: 900,
xl: 1200,
xxl: 1536,
},
},
};Image background gradient
Used for hero images that overlay text. Configurable per mode and per template:
const themeOptions: CustomThemeOptions = {
imageBackgroundGradient: {
light: {
standard: "linear-gradient(270deg, rgba(255,255,255,0) 0%, rgba(18,18,18,0.8) 100%)",
split: "linear-gradient(270deg, rgba(255,255,255,0) 0%, rgba(18,18,18,0.8) 100%)",
},
dark: {
standard: "linear-gradient(270deg, rgba(255,255,255,0) 0%, rgba(18,18,18,0.8) 100%)",
split: "linear-gradient(270deg, rgba(255,255,255,0) 0%, rgba(18,18,18,0.8) 100%)",
overlay: "linear-gradient(270deg, rgba(255,255,255,0) 0%, rgba(18,18,18,0.8) 100%)",
},
},
};Full example
import { CustomThemeOptions } from "fansunited-frontend-core";
export const brandTheme: CustomThemeOptions = {
mode: "dark",
colorSchemes: {
dark: {
palette: {
primary: {
plainColor: "#FF5722",
primaryContainer: "#FF7043",
onPrimary: "#FFFFFF",
},
},
surface: "#1A1A1A",
surfaceVariant: "#2A2A2A",
textPrimary: "#FAFAFA",
textSecondary: "#E0E0E0",
outlineEnabledBorder: "#3A3A3A",
},
},
customFontFamily: {
dark: {
primary: "Inter, sans-serif",
secondary: "Roboto, sans-serif",
},
},
};
<ClassicQuizPlay {...otherProps} themeOptions={brandTheme} />;Tips
- Set theme once, reuse everywhere — define a single
CustomThemeOptionsobject and either pass it viaThemeProvideror import it where needed. - Only the mode you use matters — if
modeis"light", thedarkcolor scheme is ignored. You can set both to support runtime mode toggling. - Partial overrides work — you only need to define tokens you want to change. Everything else falls back to the default theme.
