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:
/* 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:
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}`