Theming
Every look in the template comes from config values. Change one and the app re-themes while you watch.
The fastest route
Open the app, press t then c to open the settings drawer, and change what you like. Everything applies live and survives a reload, because the config is persisted under sa-config.
When it looks right, use Copy config in the drawer. It puts the whole live config on the clipboard as a complete defaultConfig declaration in TypeScript, ready to replace the export in src/core/config/defaultConfig.ts. Paste it there and your choices become the shipped defaults for everyone.
Light or dark is not in it, because color mode is not part of SmartAdminConfig. It lives under localStorage['sa-color-mode'], and the copied text names the mode you were in so nothing is lost silently.
The brand color
One hex becomes an eleven-stop ramp written to <html> as the CSS variables --sa-primary-50 through --sa-primary-950:
import { useConfig } from '@core/config/useConfig'
useConfig().mergeConfig({ theme: { primary: '#7c3aed' } })Set the value on the config, not on the engine. A watcher calls setPrimary for you, which is what keeps the persisted config and the screen in agreement. There are three colors: primary, secondary and accent.
Six presets ship, each one a color trio plus a radius and a density:
| Preset | Primary | Secondary | Accent |
|---|---|---|---|
| Teal | #1DB9C3 | #71717a | #0ea5e9 |
| Indigo | #4f46e5 | #64748b | #8b5cf6 |
| Emerald | #059669 | #6b7280 | #10b981 |
| Rose | #e11d48 | #71717a | #f43f5e |
| Amber | #d97706 | #78716c | #f59e0b |
| Slate | #334155 | #64748b | #0ea5e9 |
A preset only sets the keys it names, so picking one keeps your fonts, layout and language.
The page background
The shell's background is its own pair of values, one per mode, and cards stay bg-card on top of it. That is what gives the floating-card look:
useConfig().mergeConfig({
theme: {
background: { light: '#f1f2f7', dark: '#0e1013' },
},
})Fonts
Eighteen families ship in three slots: primary for the interface, arabic for right-to-left text, and mono for code. Ten Latin families, three monospace, five Arabic.
useConfig().mergeConfig({
fonts: { primary: 'Plus Jakarta Sans', mono: 'Fira Code' },
})A font outside the list is a customFonts entry: a name and the URL of its stylesheet, optionally the weights to load.
useConfig().mergeConfig({
fonts: {
customFonts: [
{ name: 'Cabinet Grotesk', url: 'https://example.com/cabinet.css', weights: [400, 600] },
],
},
})Custom fonts are registered before the font watchers run, so a persisted custom font is already there on the first paint.
Radius, density and the type scale
| Key | Values | Effect |
|---|---|---|
ui.borderRadius | none, small, medium, large | Writes --radius on <html>: 0, 0.375rem, 0.625rem, 1rem. Every corner in the UI derives from it. |
ui.density | compact, default, comfortable | Toggles a class on <html> that tightens or loosens row heights and paddings. |
typographyScale | compact, default, comfortable | Swaps the text size scale. |
Dark mode
import { useTheme } from '@core/theme/useTheme'
const { isDark, toggleDark } = useTheme()toggleDark adds or removes the dark class on <html> and stores the choice under sa-color-mode. The features.darkMode flag is one of the two that enforce rather than hide: turn it off while the app is dark and it returns to light, because shipping a dark app with no way back is the worse failure.
Layout
Four shells: classic, modern (the default), minimal and horizontal.
useConfig().setUi('layout', 'classic')The sidebar's side is ui.sidebarPosition, its shape is ui.sidebarStyle (expanded, collapsed, floating), and ui.navbarStyle is sticky or static.
Per-page overrides
A page can override config for its own lifetime with definePageConfig, which is how one screen gets a different layout without changing anyone else's.
Where to go next
- Theming system — the ramp generator, the tokens,
useThemein full. - Global config — every key and its default.