Docs
GitHub

Theming

A theme in SINA is the same tokens with different values: you redefine the --sina-* custom properties and every component pointing at them updates, with no component edits and no rebuild. The cascade contract makes overrides predictable: SINA's reset lives in @layer reset, while component and app CSS are unlayered, so your app's CSS wins by plain source order. Import your theme block after SINA's styles and it takes effect.

Build a brand theme

Override the roles that carry your brand

Usually primary, its readable foreground, hover, and maybe radius. Everything else inherits.

:root {
  --sina-color-primary: #6d28d9;
  --sina-color-primary-fg: #ffffff;
  --sina-color-primary--hover: #5b21b6;
  --sina-radius--md: 0.5rem; /* softer corners, brand-wide */
}

Or let the typed helper emit it

createTheme() in @sina-design-system/theme generates the block and type-checks which roles you're allowed to touch:

import { createTheme } from "@sina-design-system/theme";

const acme = createTheme(
  { colors: { primary: "#7c3aed" }, radius: { md: "0.5rem" } },
  { selector: '[data-theme="acme"]' },
);

Scope it

Put the block on :root for one brand, or under [data-theme="acme"] for several. Scoped brands compose with dark mode. For a local override, themeVars() returns the same tokens as an inline-style object for style={}.

Start small

Most brands override a handful of roles (primary, the surfaces, maybe radius) and inherit the rest.

Dark mode

theme.css ships a [data-theme="dark"] block that reassigns the same roles. Flip data-theme on <html> and the whole UI recolors; a light / dark / system toggle is just writing that attribute and remembering the choice in localStorage. Both palettes are held to WCAG AA contrast by build guards in the theme package.

The locked boundary

Not every token is yours to change, and that boundary is itself a governance decision.

Roles
Brand-openbg / surface* / text* / border*, primary* / secondary*, success / warning / info, chart series, radius / space / text / fontYours. Rebrand freely.
Governance-lockeddanger*, the secure-surface tint, the focus ring, the intent-* aliasesReserved. The typed API refuses them.

Why lock anything?

A brand theme must never make an error look calm, hide the focus ring from keyboard users, or disguise a secure-action affordance. Those aren't style; they're safety and accessibility signals. createTheme() rejects a locked role at compile time. (Raw CSS can still force a variable by specificity. The API simply won't help you do it.)

Do

  • Override primary, surfaces, and radius to match your brand
  • Use createTheme() so locked roles are caught at compile time
  • Re-check contrast after a heavy rebrand

Don’t

  • Restyle danger to look friendly; errors must read as errors
  • Remove or dim the focus ring
  • Hard-code hex in a component to dodge the token system

Next: Tokens, the vocabulary every theme re-values.