Theming

Every visual decision is a CSS custom property. Redeclare a token after the framework loads and every component that reads it repaints, including the hover, active, and focus states that derive from it.

The token surface

Colors are --color-*, geometry is --radius-* and --shadow-*, spacing is --spacing, and type is --font-*; a few customs with no Tailwind namespace stay on --st-*. Components read these through var(), so an override takes effect with no rebuild. The Specification catalogs every token and what it controls; this page is about changing them.

Two rules carry over from that catalog. The shipped values are OKLCH literals because states derive at runtime via color-mix(in oklch, …), and OKLCH mixing stays vivid through the midpoints where sRGB tends to muddy a brand color; your own overrides can use any representation (HSL, hex, sRGB). And every background-providing token has a paired -foreground, so if you override one, override the other and text contrast survives the base change.

Setting up overrides

An override is just a token you redeclare after the framework loads. There are two setups depending on whether you run a build step; both end at the same place, with your value winning over the shipped default. The same two setups recur across these pages, so it’s worth fixing them once here.

Without a build step

Write a plain stylesheet, load it after the framework bundle, and declare the tokens you want to change on :root. Nothing to compile.

overrides.css

Load order is the whole trick. Your file comes after stisla.css, so at equal specificity your :root wins. No !important, no build.

With Vite and Tailwind

If you compile your own CSS with Tailwind, import the source theme rather than the precompiled bundle, then redeclare the tokens inside @theme so they land in the same layer as the shipped ones and regenerate any matching utilities. Tailwind comes in once, at the top. For the dark delta, use the registered dark variant instead of hand-writing the selector (see Dark mode).

app.css

Either way, the result is the same. Pick the smallest scope that covers what you need; the next section breaks the scopes down.

The override here is scoped to a wrapper so both states sit side by side, but a real :root or @theme override repaints exactly the same way, app-wide. Hover the buttons to confirm the derived hover and active states track the new brand color too.

Source

The shipped defaults live in packages/style/src/theme.css. Read that file when you want a starting copy with the current values to paste and edit.

Override scopes

The cascade gives you four scopes for any token override. Same mechanism (CSS variable plus inheritance) at every level. Specificity never fights you because variables inherit; selectors don’t. The scenario below is the same in each example. Turn the primary brand color green. Pick the smallest scope that covers what you actually need.

Global

Set the token on :root in a stylesheet that loads after stisla.css (or in @theme with a build). The change applies to the whole app.

Theme variant

Same write, scoped to a theme attribute. Toggle data-theme="brand" on <html> or any wrapper to switch in.

Scoped wrapper

Define the token on a wrapper class. Every primary-tinted component inside the wrapper picks up the new value, because the variable inherits down the tree. Hover the buttons below to see hover and focus derivations track the override.

Single element

Write the variable inline on the element. Inline always wins on specificity, no !important needed.

Pick the smallest scope that still covers what you need. A one-off brand button doesn’t need a whole-app retheme. A multi-section dashboard doesn’t need an inline write on every element.

One more override path lives alongside the token surface: component-scoped variables (--button-radius, --dialog-bg, --alert-tone) that fall back to global tokens. Reach for those when you want one shape’s knob to differ from the global default. The Styling page covers restyling existing components, and the Optimization page has the recipes for forking a bundle or building a custom entry.

Brand color

The most common change. A single --color-primary override repaints every primary-toned surface. Hover, active, focus ring, and the --color-highlight tint all derive from it via color-mix at runtime, so one value carries the whole brand. No rebuild, no second pass.

Each intent also ships a paired --color-<intent>-emphasis, a darker, hand-tuned shade for the intent used as foreground marks (text, icons, links, soft chips) on a plain or tinted surface, rather than as a fill. It clears AA contrast where a runtime mix could not. Because it is a separate token, a strong retone of --color-primary should set --color-primary-emphasis too, otherwise those marks on tinted backgrounds can fall below AA contrast.

app.css

The same shape works for any hue. Picking one is a design decision (perceptual lightness, chroma headroom, accessibility against the foreground). A few starting points to paste and tune:

Brand--color-primary
Violetoklch(0.58 0.22 285)
Tealoklch(0.65 0.13 195)
Amberoklch(0.78 0.16 75)
Roseoklch(0.65 0.21 13)

Change --color-primary-foreground at the same time so text on filled primary surfaces stays readable. Most brand hues pair with oklch(1 0 0) (pure white); a pale yellow brand would pair with a darker foreground.

Density

One token shrinks or grows every component proportionally. Component padding, gap, and height are all multiples of the spacing base --spacing through the --spacing() helper, so changing the base reaches every spacing decision in the system at once. Chip-like components (buttons, form fields, pagination chips, navbar buttons, toggles, tabs) read their height the same way, so the box grows on both axes together.

SettingValueUse
Compact0.2remInformation-dense apps, admin dashboards, tabular UIs
Default0.25remGeneral-purpose product UI
Comfortable0.28remMarketing surfaces, content-first reading flows
app.css

The base is a single length you can dial freely. 0.22rem for a slight squeeze, 0.27rem for a slight stretch. The whole system retunes at runtime. For tuning one component’s size without touching the global base, see the sizing model in Styling.

Changing several tokens at once

The changes above move one axis each. Move several together and you get a distinct look; the whole character shifts. This worked example retunes radius, shadow, border weight, border color, and typography at once, shown across a mix of surfaces so it reads as a system. Nothing here is a special “preset” mechanism; it’s the same token overrides, just more of them on one wrapper.

app.css

The look comes from pushing every axis at once. Radius tiers go to zero; the shadow tiers all step up to a solid 6 px offset tracking --color-foreground so it flips per theme; --color-border moves from the soft hairline grey to full-contrast foreground; --st-border-width doubles so every shape outline reads thicker; Space Grotesk replaces the neutral Inter. Typographic weight isn’t a global token, so the heading bumps are written by class selector. The wrapper here scopes it to a region; move the same tokens to :root (or @theme with a build) to make it the app-wide default.

Dark mode

Light and dark are deltas on the same token surface. Stisla ships a dark block that flips every surface, neutral, and interactional token; intents stay (a green button is the same green in dark mode). The dark block keys off [data-theme="dark"] or the .dark class on <html>, so React or Vue apps with localStorage-based toggling work out of the box.

To roll your own dark theme, override the same tokens. You only need to redeclare what you’re changing. Pick the way that matches your setup.

Without a build step. Write the override under the dark selectors directly.

With Vite and Tailwind. Use the registered dark variant instead of hand-writing the selector. It compiles to the same [data-theme="dark"] / .dark rule.

Two tokens stay theme-independent on purpose. --color-warning-foreground keeps a dark cap because yellow stays yellow and text on it should stay dark in both modes. --color-overlay and --color-overlay-foreground stay constant because they paint chrome on top of imagery, which can be anything.

To remember the choice and avoid a flash of the wrong theme on load, store the preference and apply it before the stylesheet renders, in a small inline script at the top of <head>.

Your toggle then sets the same attribute and writes the choice back.