Theming

Widgets never hardcode a color, radius, or font - they render through CSS variables. Rebranding a client app is overriding variables in one file.

How the tokens are layered

src/styles/signalos.css (pulled automatically with any widget) has three layers - override at whichever level matches your intent:

  • Primitives - raw OKLCH colors: --color-brand-teal, --color-navy-800, full status scales. Override these to re-brand globally.
  • Semantics - meanings, themed light and dark: --bg-card, --text-secondary, --sidebar-bg. Override these to restyle one surface without touching the palette.
  • Tailwind mapping - exposes everything as utilities (bg-card, text-text-secondary, border-sidebar-border). You rarely touch this layer.

Rebranding a client

After the tokens import, override what the client owns - every widget and page restyles instantly:

src/styles/client-brand.css
/* imported AFTER signalos.css */
:root {
  --color-brand-teal: oklch(0.55 0.2 145);        /* client accent */
  --color-brand-teal-hover: oklch(0.48 0.18 145);
  --radius-lg: 0.75rem;                            /* rounder cards */
}

Never edit signalos.css itself - it gets replaced on signalos update tokens. Overrides live in your own file and survive every update.

Dark mode

Class-based: the .dark class on <html> re-maps every semantic token (usually toggled with next-themes). Widgets need no dark-mode code - if your overrides only touch primitives, both themes update together.

Fonts

The tokens expect --font-inter and --font-ibm-plex-mono with system fallbacks. In Next.js:

src/lib/fonts.ts
import { IBM_Plex_Mono, Inter } from "next/font/google"

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" })
const mono = IBM_Plex_Mono({
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
  variable: "--font-ibm-plex-mono",
})

export const fontVariables = `${inter.variable} ${mono.variable}`

Next