Theming

Change the theme tokens.

How the system works

The theme has two layers. Values are CSS variables in globals.css, named for what a colour is for, such as --card. Utilities map each variable to a class in @theme inline, so --card becomes bg-card. Components use only the utilities, so changing a value restyles every component without touching its file.

VariableUtilityRead by
--background#fffbg-backgroundPage canvas
--primary#0090ffbg-primary text-primary-foregroundFilled buttons, switches, checks, selection
--border#00000017border-borderEvery divider and edge
--radius0.5remrounded-sm … rounded-3xlAll corners
--shadow-defaulthairline + 1px dropshadow-raisedResting cards and controls
--text-ui0.8125remtext-uiButtons, inputs, menus, nav

Dark mode works the same way. .dark on <html> points the same variables at dark values, so a component written once is right in both modes. The accent is eight of those variables, --primary to --ring, which a preset swaps together.

  1. Map each mode
    Put light values on :root and dark values on .dark, under the same names.
  2. Use semantic utilities
    Write bg-card or text-muted-foreground, never palette colours or hex.
  3. Keep it central
    Skip dark: colour overrides in components, so a theme change stays one edit in one file.

The same file also holds these tokens.

  • --radiusOne base that every corner multiplies, so nested corners stay concentric.
  • text-ui-*The type scale for text inside components.
  • --shadow-*Four shadow levels behind shadow-xs to shadow-xl and the named elevation.
  • --font-*The UI, code and reading font stacks.
  • --shellColours for app chrome around the content panel.
  • --scrollbarScrollbar thumb colours.
pnpm dlx shadcn@latest add @paneui/theme

@paneui/theme writes the variables below and installs @paneui/utils, whose cn() merges the text-ui-* sizes as font sizes. For a tinted wash, use the -subtle token, such as bg-success-subtle.

globals.css (excerpt)css

Colors

Colours are named by role, and each surface has a matching -foreground token for text on it. Use tokens in components, never palette colours (bg-zinc-100) or hex values. Every token's light and dark value is on Customize.

  • background
  • card
  • surface-sunken
  • surface-standout
  • primary
  • primary-subtle
  • secondary
  • muted-foreground
  • fg-tertiary
  • border
  • border-strong
  • ring
  • destructive
  • success
  • warning
  • chart-1…8
  • shell

Dark mode

Put .dark on <html> to switch every token. The theme declares the variant as @custom-variant dark (&:is(.dark *)). Set color-scheme at the same time so native scrollbars and form controls follow. Pair primary with text-primary-foreground, never text-white, because the neutral preset makes it near-white in dark mode.

theme-script.jsjs

To let users switch, use Theme Toggle, which stores the choice for the script above.

Accent presets

The accent is eight role variables per mode, blue by default: primary and its -hover, -active, -foreground, -subtle, -subtle-hover and -border variants, plus ring. Every preset reads the same steps of its own 12-step scale, so surfaces, text and charts stay the same. Install one after the theme.

  • theme-neutral
  • theme-bluedefault
  • theme-green
  • theme-orange
  • theme-rose
  • theme-violet
pnpm dlx shadcn@latest add @paneui/theme-violet

An installed preset overwrites the variables on :root and .dark, so the accent is fixed. To let users switch, keep the default roles on :root, add one block per accent keyed on an attribute, and apply the saved choice in the same pre-paint script as dark mode.

For a brand colour, use the picker on Customize to derive the eight roles and copy them.

globals.csscss

Typography

Use the text-ui* sizes inside components, and keep text-sm to text-2xl for reading copy and page headings. Set titles in font-semibold, labels and buttons in font-medium, and use font-semimedium (450) where 400 reads too light on a dense row.

ClassSizeUse for
text-ui-2xs10pxTiny counters and labels.
text-ui-xs11pxMeta text, shortcuts and counts.
text-ui-sm12pxCaptions, badges, small controls, tooltips.
text-ui13pxThe default for controls, menus and text inside components.
text-ui-lg14pxLarge controls and card titles.

Fonts

The theme names its faces in --font-sans, --font-mono and --font-serif, each with a CJK face and the system stack behind it. Without the font files it renders in system fonts, so installing them is optional.

UtilityFaceUse for
font-sansInter, Noto Sans SCAll UI text. Inter's ss03 set is on.
font-monoJetBrains MonoCode, keys, terminals and tabular IDs.
font-serifNewsreader, Noto Serif SCLong-form reading (the reading typeset preset).
pnpm dlx shadcn@latest add @paneui/fonts

This adds the font packages and packages/tokens/styles/fonts.css; import that file once in your root layout. Each face is split by unicode range, so a page downloads only the glyph sets it shows. For code that lines up with Chinese text, install a CJK-aligned monospace such as Sarasa Gothic or Maple Mono yourself and put it first in --font-mono.

Radius

Every step is a multiple of --radius, so change the base and the whole scale follows. No step goes past --radius-max (16px): larger corners turn controls into pills and push nested parts apart. Keep nested corners concentric, with the inner radius equal to the outer radius minus the padding between them. That is why p-1 menus (rounded-2xl) hold rounded-lg rows. Use rounded-full only for shapes that are actually round. Sizes below are at the default 0.5rem; try other values on Customize.

PreviewClassSizeUse for
rounded-xs2pxSearch highlights, favicons and tiny segments.
rounded-sm4pxCheckboxes, extra-small buttons, keys and small chips.
rounded-md6pxTab indicators inside a p-0.5 list.
rounded-lg8pxControls and menu rows.
rounded-xl10pxCards, tables and code blocks.
rounded-2xl12pxMenus and popups, two-layer inset cards.
rounded-3xl16pxDialogs, drawers and big panels.
rounded-4xl16pxKept for older code; sits at the 16px cap.
rounded-5xl16pxKept for older code; sits at the 16px cap.

Elevation

Four shadow levels back Tailwind's shadow-xs to shadow-xl, so plain utilities pick them up. Give resting cards, tables and code blocks shadow-raised, never a floating level. Show hover on rows, menu items and ghost buttons with a fill (bg-secondary), not a ring.

PreviewClassTokenUse for
shadow-xs--shadow-borderA 1px ring, no lift.
shadow-sm--shadow-defaultResting controls and cards.
shadow-lg--shadow-lgMenus, popovers and floating panels (shadow-md is the same).
shadow-xl--shadow-xlDialogs, drawers and sheets.

Components read these named levels, each built on the scale above.

shadow-raisedResting controls and cards (reads shadow-sm). Hover steps to shadow-raised-hover.
shadow-surfacePopovers, menus and floating panels (reads shadow-lg).
shadow-wellInset hairline for fields and tracks.
shadow-chipKeys and small chips.
shadow-primaryFilled primary buttons (none in dark).

Native style rules

Follow these rules when you build a new component.

  • secondary, muted and accent are fills; use bg-card, bg-popover or bg-background for anything that must be opaque.
  • No gradient sheens, inset highlights, text shadows or glows.
  • Size buttons and inputs at h-8 (sm h-7, xs h-6) and menu rows at 26px. Show disabled with opacity-64.
  • Answer a press on standard controls with a fill change, not a scale. Popups enter from scale-98 and opacity.
  • Show focus with focus-visible:ring-2 focus-visible:ring-ring/50.
  • Put app chrome on bg-shell and the content in a shell-inset panel.

Adding tokens

Define a new token with a value for both modes, then map it in @theme inline to get the utilities.

globals.csscss