Skip to content

Theming ​

Every look in the template comes from config values. Change one and the app re-themes while you watch.

Open in demo

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:

ts
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:

PresetPrimarySecondaryAccent
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:

ts
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.

ts
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.

ts
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 ​

KeyValuesEffect
ui.borderRadiusnone, small, medium, largeWrites --radius on <html>: 0, 0.375rem, 0.625rem, 1rem. Every corner in the UI derives from it.
ui.densitycompact, default, comfortableToggles a class on <html> that tightens or loosens row heights and paddings.
typographyScalecompact, default, comfortableSwaps the text size scale.

Dark mode ​

ts
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.

ts
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 ​