Colors

The semantic colour tokens every component draws from, what each one is for, and how to change them.

Nothing in PanelUI names a colour. A Button does not know it is dark grey; it knows it is primary, and what primary resolves to is decided by the active theme. That indirection is the whole reason a theme switch restyles the library rather than only the screens you wrote.

So there is one rule, and it applies to your own code as much as to ours: use the semantic token, not the palette value. bg-primary, not bg-neutral-800. The moment a raw value appears, that element stops following the theme and starts being a bug someone finds in dark mode.

The tokens

Most come in pairs — a surface and the text that goes on it. foreground is always the readable colour for the token it is named after, so bg-card text-card-foreground is guaranteed to have contrast in every theme.

Surfaces

TokenUsed for
background / foregroundThe page itself, and its text
card / card-foregroundRaised surfaces standing on the page
popover / popover-foregroundOverlays — dialogs, sheets, toasts, tooltips
overlay / overlay-foregroundFloating panels: menus, and anything opened on top of an overlay

overlay is one step further forward than popover, and it exists because a menu is usually the thing on top of a popover. Without a step of its own it disappears into whatever it opened over.

Elevation

TokenUsed for
surfaceThe grouping surface behind Frame
surface-secondaryOne step further from the page
surface-tertiaryTwo steps further

A ladder rather than one value, so nesting reads as hierarchy. This describes depth within the page; overlay describes leaving it.

Actions and state

TokenUsed for
primary / primary-foregroundThe main action colour
secondary / secondary-foregroundSecondary fills
muted / muted-foregroundQuiet fills, and supporting text
accent / accent-foregroundHover and selected states
borderHairlines
inputField borders
ringFocus rings
skeletonThe shimmer fill

Status

destructive, info, success and warning, each with a -foreground, a -soft and a -subtle.

SuffixUsed for
(none)The saturated colour — an icon, a border, a filled button
-foregroundText on a tinted fill. Darker than the base colour, so it is readable on one
-softThe tint behind an Alert
-subtleThe tint behind a Badge, one step quieter

The tints are real tokens rather than opacity modifiers, and that is not an accident. dark: only resolves inside the light and dark themes — inside a named theme it compiles to a class selector that never matches, so a dark:bg-info/[0.06] tint silently vanishes the moment someone switches to Moon. See Theming for why.

Charts

chart-1 through chart-5, ordered by prominence. A separate ramp from the status colours because they answer a different question: a status colour means something — this failed, this is fine — while a series colour only has to be told apart from the other four.

chart-1 is the series the chart is about, and every theme starts it on that family's own accent. LineChart takes chart-1 by default and each further series takes the next, so putting every chart in an app on brand is five lines in your global.css.

Syntax

code-keyword, code-string, code-number, code-comment, code-function, code-property, code-punctuation, code-inserted and code-deleted — the highlighting ramp CodeBlock and Response draw from.

Changing a colour

Redefine the token in your own global.css, in the same @variant shape the library uses:

global.css
@import 'panelui-native/theme.css';

@layer theme {
  :root {
    @variant light {
      --color-primary: #4f46e5;
      --color-primary-foreground: #ffffff;
    }
    @variant dark {
      --color-primary: #818cf8;
      --color-primary-foreground: #1e1b4b;
    }
  }
}

Every component drawing bg-primary follows, including ones you have not written yet.

Two rules Uniwind enforces

Every theme must define the same variables. Adding a token to one theme without adding it to all of them fails the build with "All themes must have the same variables".

The web's :root { --x } / .dark { --x } pattern does not work. Uniwind resolves themed tokens only from @variant blocks, and light-dark() inside @theme breaks colour parsing outright.

If you are overriding for all six themes rather than two, the same block takes moon, moon-dark, grass and grass-dark alongside light and dark.

Reading a colour in JavaScript

Some things cannot be given a class: an SVG fill, a placeholderTextColor, a prop on a third-party component. Resolve the token instead of hardcoding what it currently evaluates to:

import { useCSSVariable } from 'uniwind';

const muted = useCSSVariable('--color-muted-foreground');

<Svg><Circle fill={muted} /></Svg>

It subscribes to theme changes, so the value updates on every switch — which a hex literal copied out of this page would not. Icons inside PanelUI's own coloured surfaces already do this for you; see Button for how startContent icons pick up a readable colour.

Next

On this page