August 16, 2026 · 3 min read
"Semantic tokens: why you theme meanings, not components"
"Theme Kit themes semantic tokens — background, foreground, primary, muted — instead of component classes. Here's why that scales to every framework and every component library."
Component-based theming asks "what color is a button?" and then writes .button { background: #6366f1 }. That works until you have three button variants, a dark mode, and a second component library to theme. Semantic tokens flip the question: theme meanings, and let every component resolve its own look from them.
Tokens, not hex values
Instead of hard-coding colors into components, you define a small set of semantic meanings:
1import { defineTheme } from "@theme-kit/core";
2
3export const lightTheme = defineTheme({
4 name: "app-light",
5 meta: { family: "app", mode: "light" },
6 tokens: {
7 colors: {
8 background: "#fafafa",
9 foreground: "#18181b",
10 card: "#ffffff",
11 primary: "#6366f1",
12 primaryForeground: "#ffffff",
13 muted: "#f4f4f5",
14 mutedForeground: "#71717a",
15 destructive: "#ef4444",
16 destructiveForeground: "#ffffff",
17 success: "#16a34a",
18 successForeground: "#ffffff",
19 border: "#e4e4e7",
20 ring: "#6366f1",
21 },
22 radius: { lg: "12px" },
23 },
24});Components consume the meaning — primary, muted, border — never a literal color. When the theme changes, every consumer updates together.
The cascade: tokens → CSS variables → components
At runtime Theme Kit flattens tokens into CSS custom properties:
--theme-color-background: #fafafa;
--theme-color-primary: #6366f1;
--theme-color-mutedForeground: #71717a;
--theme-radius-lg: 12px;Your components reference the variables (or the framework hook), so the whole tree re-themes in one DOM update:
1import { useThemeTokens } from "@theme-kit/react";
2
3function Card() {
4 const tokens = useThemeTokens();
5 return (
6 <div
7 style={{
8 background: tokens.colors?.card,
9 borderColor: tokens.colors?.border,
10 }}
11 >
12 {children}
13 </div>
14 );
15}Why meanings scale
- One theme, many surfaces —
primarymeans "the main action color" everywhere: buttons, links, focus rings, active states. - Dark mode for free — swap the token set, not the components. A component written against tokens renders correctly in both modes with zero changes.
- Framework-agnostic — the same tokens feed React, Vue, Svelte, Angular, Web Components, and CSS-only Tailwind projects. The runtime is the shared contract.
- Component-library adapters — Theme Kit can map tokens into MUI, Chakra, Ant Design, shadcn/ui, Bootstrap, and more, because those libraries already accept token-ish inputs.
Extending tokens safely
Tokens are plain objects, so they compose:
1import { extendTheme } from "@theme-kit/core";
2
3// Add a brand family without touching the base
4export const brandLight = extendTheme("app-light", {
5 meta: { family: "brand", mode: "light" },
6 colors: {
7 primary: "#0ea5e9", // override just what you need
8 },
9});extendTheme merges deeply — nested colors, radius, shadows, and the code block all inherit unless overridden. You can layer a family, a mode, or a brand on top of a base theme.
Semantic tokens and accessibility
Because contrast pairs are semantic (foreground on background, primary on primaryForeground), Theme Kit can audit them:
import { validateThemeContrast } from "@theme-kit/core";
const result = validateThemeContrast(theme, { themes });
// { valid, checks: [{ foregroundToken, backgroundToken, ratio, passesAALarge, … }] }A component that consumes foreground on background is automatically checked — no manual mapping from "component X uses color Y" required.
The whole docs site you're reading runs on this: every card, button, and code block reads from the live theme's tokens. That's the semantic-token payoff — theming that stays honest at scale.