@hex-core/tokens
HSL design tokens, theme presets, and CSS/Tailwind transformers.
Install
1.3.6Runtime dependencypnpm add @hex-core/tokensWhat it does
`@hex-core/tokens` is the design-token data layer. It exports three theme objects (`defaultTheme`, `midnightTheme`, `emberTheme`) plus four transformers that turn those objects into the formats your build pipeline understands: CSS, Tailwind config, raw token sets, and runtime-injectable scoped CSS.
Tokens are the *names* that components reference (`--color-primary`, `--space-4`). Themes are the *values* assigned to those names. The transformers are the bridges. Components, the studio, and the MCP server all read from this single source.
Two namespaces exist intentionally: `themeToCss` emits raw triplets (`--primary: 240 5.9% 10%`) for alpha-composition utilities; `themeToScopedRuntimeCss` emits both the raw form and the `--color-<key>: hsl(...)` form Tailwind v4 expects in `@theme`. Don't hand-roll the second.
Public API
defaultTheme / midnightTheme / emberTheme
import { defaultTheme, midnightTheme, emberTheme } from "@hex-core/tokens";
import type { Theme } from "@hex-core/registry";
const t: Theme = defaultTheme; // strict; tokens align with @hex-core/registry@^0.3.2Three built-in theme objects shaped against `@hex-core/registry`'s strict `Theme` type. Each carries `tokens.light`, `tokens.dark`, and metadata. New presets land in `@hex-core/themes`, not here — `tokens` ships only the foundational three.
import { defaultTheme } from "@hex-core/tokens";
console.log(defaultTheme.tokens.light.primary);
// → { value: "222 25% 18%", type: "color" }themeToCss
function themeToCss(theme: Theme): string;Emit the raw-triplet CSS namespace (`--<key>: <H> <S>% <L>%`) for both modes in one pass. The output always wraps `:root {}` (light) and `.dark {}` (dark) in an `@layer base { ... }` block — there's no options arg to pick a single mode. Useful when you need alpha composition (`hsl(var(--primary) / 0.5)`). For mode-aware single-palette output (e.g. runtime overrides on a scoped surface), reach for `themeToScopedRuntimeCss`, which does accept options.
import { themeToCss, defaultTheme } from "@hex-core/tokens";
const css = themeToCss(defaultTheme);
// → "@layer base { :root { --background: 210 20% 98%; ... } .dark { --background: 240 10% 4%; ... } }"themeToScopedRuntimeCss
function themeToScopedRuntimeCss(theme: Theme, options?: {
mode?: "light" | "dark";
scope?: string;
}): string;Emit BOTH namespaces in one rule: raw `--<key>: <value>;` AND `--color-<key>: hsl(<value>);`. Designed for runtime overrides (think: a studio that mutates the canvas without rebuilding Tailwind). Non-color tokens emit only the raw form.
import { themeToScopedRuntimeCss, defaultTheme } from "@hex-core/tokens";
const css = themeToScopedRuntimeCss(defaultTheme, {
mode: "dark",
scope: ".studio-canvas",
});
// → ".studio-canvas.dark { --primary: 0 0% 98%; --color-primary: hsl(0 0% 98%); ... }"themeToTailwindConfig
function themeToTailwindConfig(theme: Theme): Record<string, Record<string, string>>;Generate a flat top-level map of Tailwind theme slots — `colors`, `borderRadius`, `spacing`, `fontSize`, `transitionDuration`, `height` — each a `Record<string, string>` of token names to CSS-variable references. Spread it under `theme.extend` in `tailwind.config.ts` if you're still on Tailwind v3 (each returned key maps to its matching `theme.extend.*` slot — `colors` → `theme.extend.colors`, `borderRadius` → `theme.extend.borderRadius`, etc.). Tailwind v4 consumers prefer the `@theme {}` block instead.
import { themeToTailwindConfig, defaultTheme } from "@hex-core/tokens";
export default {
content: ["./src/**/*.{ts,tsx}"],
theme: { extend: { ...themeToTailwindConfig(defaultTheme) } },
};generateGlobalsCss
function generateGlobalsCss(theme: Theme, options?: {
target?: "v3" | "v4"; // defaults to "v3"
}): string;One-shot helper that emits the full `globals.css` body. The `target` option picks the Tailwind major: `"v3"` (default, backward compatible) emits `@tailwind base/components/utilities` + `@layer base { :root {} .dark {} }` blocks (pair with a `tailwind.config.ts` that maps the raw `--<key>` variables to color/radius utilities); `"v4"` emits `@import "tailwindcss"` + `@theme { --color-<key>: hsl(...) }` so utilities like `bg-background` resolve directly with no `tailwind.config.ts` needed. Used by the CLI's `hex init` and the MCP `emit_app_context` tool.
import { generateGlobalsCss, defaultTheme } from "@hex-core/tokens";
import { writeFileSync } from "node:fs";
// Tailwind v4 (recommended for new projects).
writeFileSync("./globals.css", generateGlobalsCss(defaultTheme, { target: "v4" }));
// Tailwind v3 (default — emits @tailwind directives + @layer base).
writeFileSync("./globals.v3.css", generateGlobalsCss(defaultTheme));getTheme / listThemes
function getTheme(name: string): Theme | undefined;
function listThemes(): Array<{ name: string; displayName: string; description: string }>;Enumerate the three built-in themes (`default`, `midnight`, `ember`) by name. `getTheme` returns the full Theme object or `undefined`; `listThemes` returns just metadata. The studio's theme switcher uses these; so do the MCP / CLI surfaces that don't want to add a separate `@hex-core/themes` dep just to look up a name.
import { getTheme, listThemes } from "@hex-core/tokens";
const theme = getTheme("midnight") ?? getTheme("default");
console.log(listThemes().map((t) => t.name));
// → ["default", "midnight", "ember"]themeToFlatJson
function themeToFlatJson(theme: Theme, mode?: "light" | "dark"): Record<string, string>;Flatten a theme into a single key-value record (`{ "--primary": "240 5.9% 10%", "--radius": "0.375rem", ... }`) suitable for Style Dictionary interop, Figma Variables import, or any pipeline that wants raw token values without the `{ value, type }` wrapper. Keys are the CSS-variable form (prefixed with `--`).
import { themeToFlatJson, defaultTheme } from "@hex-core/tokens";
const flat = themeToFlatJson(defaultTheme, "light");
console.log(flat["--primary"]); // → "222 25% 18%"
console.log(flat["--radius"]); // → "0.375rem"Color derivation utilities
function deriveForegroundFor(bgValue: string, opts?: DeriveForegroundOptions): ColorToken;
function deriveDarkFromLight(light: TokenSet, opts?: DeriveDarkOptions): TokenSet;
function deriveSecondaryFromPrimary(primaryValue: string, opts?: DeriveSecondaryOptions): ColorToken;
function colorInputToTokenValue(input: string): string | null;
function contrastRatio(fgValue: string, bgValue: string): number;
function tokenLuminance(value: string): number;Pure HSL-triplet color math used by the brand-onboarding flow. `deriveForegroundFor` picks near-white or near-black per WCAG contrast; `deriveDarkFromLight` mirrors a light-mode set into a coherent dark-mode set (with foregrounds re-derived for AA); `deriveSecondaryFromPrimary` produces the muted-but-related fill Cancel/Save-Draft buttons want; `colorInputToTokenValue` parses anything CSS-color-spec accepts (hex, named colors, `hsl(...)`) into the canonical HSL-triplet shape; `contrastRatio` + `tokenLuminance` are the WCAG building blocks. All zero-DOM, zero-React.
import {
defaultTheme,
deriveDarkFromLight,
deriveForegroundFor,
deriveSecondaryFromPrimary,
colorInputToTokenValue,
} from "@hex-core/tokens";
// User pastes a hex from Figma → snapped to the canonical token shape.
const primary = colorInputToTokenValue("#0F62FE"); // "219 99.2% 52.7%"
// Pair the primary with an AA-contrast foreground.
const fg = deriveForegroundFor(primary!).value;
// Build the secondary fill that lives next to the primary.
const secondary = deriveSecondaryFromPrimary(primary!);
// Ship a coherent dark-mode set without authoring it manually.
const darkTokens = deriveDarkFromLight(defaultTheme.tokens.light);buildTokenSet (with TokenSetSeeds)
interface TokenSetSeeds {
primary: string; // HSL triplet, brand color
foreground: string; // page text default
background: string; // page canvas
destructive: string; // error / delete CTA
radius: string; // rem string, e.g. "0.625rem"
}
function buildTokenSet(seeds: TokenSetSeeds, mode: "light" | "dark"): TokenSet;Five-seed → full TokenSet builder. The same helper the CLI's `hex theme init -i` flow uses and the same one the brand-derived preset import script reaches for; both paths produce identical output given identical seeds. Picks the right surface/framing band for the chosen mode so derived neutrals (`secondary` / `muted` / `accent` / `border` / `input`) read correctly on light vs. dark canvases.
import { buildTokenSet } from "@hex-core/tokens";
const lightTokens = buildTokenSet(
{
primary: "217 100% 53%",
foreground: "240 10% 3.9%",
background: "0 0% 100%",
destructive: "0 84% 60%",
radius: "0.625rem",
},
"light",
);
console.log(Object.keys(lightTokens).length); // 20 (19 color slots + radius)Semantic tokens layer (defaultSemanticTokens / resolveSemanticToken)
import type { SemanticTokenSet } from "@hex-core/registry";
declare const defaultSemanticTokens: SemanticTokenSet;
function resolveSemanticToken(reference: string, tokenSet: TokenSet): TokenValue | null;The **intent layer** over raw tokens. Each entry references a raw token via flat `{token-name}` syntax and adds a `useWhen` sentence describing the UI-element class it's the right choice for. Names follow `<component>.<state-or-slot>.<role>` (e.g. `button.primary.bg`, `dialog.overlay.bg`). Theme-portable — swapping the underlying theme automatically shifts every semantic entry without per-theme rewrite. Use `resolveSemanticToken` to dereference a `{token-name}` placeholder against a concrete token set.
import { defaultSemanticTokens, defaultTheme, resolveSemanticToken } from "@hex-core/tokens";
const entry = defaultSemanticTokens["button.destructive.bg"];
console.log(entry.useWhen);
// → "irreversible actions: delete, archive, deactivate, leave, force-quit"
const resolved = resolveSemanticToken(entry.value, defaultTheme.tokens.light);
console.log(resolved?.value); // → "0 65% 43%"Chart palette (chart-1 … chart-6)
// Six rotating chart-series colors per mode, accessed as CSS variables:
// var(--chart-1, var(--primary)) // first series, falls back to primary
// var(--chart-2, var(--primary)) // second series
// ... through var(--chart-6, var(--primary))
//
// Each entry is a regular ColorToken in defaultTheme.tokens.{light,dark}:
defaultTheme.tokens.light["chart-1"]; // { value: "12 76% 61%", type: "color" }
defaultTheme.tokens.dark["chart-1"]; // dark-mode counterpartSix-color chart series palette landed in 1.3.3, both modes. Mapped to CSS variables `--chart-1` through `--chart-6` with a `var(..., var(--primary))` cascade-safe fallback so chart components keep rendering even on themes that haven't authored chart slots. The dark-mode variants ship pre-tuned for `--card` / `--border` lift so SVG-rendered chart cards stay visible against dark canvases.
import { defaultTheme } from "@hex-core/tokens";
// Inspect the six rotating series colors
for (let i = 1; i <= 6; i++) {
const key = `chart-${i}` as const;
console.log(key, defaultTheme.tokens.light[key]?.value);
}
// In CSS, prefer the variable form so theme overrides cascade:
// .my-chart-bar:nth-child(1) { fill: var(--chart-1, var(--primary)); }
// .my-chart-bar:nth-child(2) { fill: var(--chart-2, var(--primary)); }sharedTokens
import type { TokenValue } from "@hex-core/registry";
declare const sharedTokens: Record<string, TokenValue>;The cross-mode token slice — radius, spacing, typography — that doesn't change between light and dark. Spread it into both palettes when authoring a new theme so you don't drift on the non-color foundations.
import { sharedTokens, defaultTheme } from "@hex-core/tokens";
import type { Theme } from "@hex-core/registry";
const myTheme: Theme = {
...defaultTheme,
name: "brand",
tokens: {
light: { ...sharedTokens, /* color overrides */ },
dark: { ...sharedTokens, /* color overrides */ },
},
};COLOR_MODE_BANDS / RADIUS_PRESETS
export const RADIUS_PRESETS = {
sharp: "0.25rem",
balanced: "0.625rem",
soft: "0.875rem",
} as const;
export type RadiusPreset = keyof typeof RADIUS_PRESETS;
export const COLOR_MODE_BANDS = {
light: { surface: 0.959, framing: 0.9 },
dark: { surface: 0.159, framing: 0.16 },
} as const;
export type ColorMode = keyof typeof COLOR_MODE_BANDS;Enumerable constants the studio's preset switcher and the CLI's interactive theme authoring depend on. `RADIUS_PRESETS` carries the three canonical chrome densities (Linear-style sharp, shadcn-canon balanced, friendly soft); `COLOR_MODE_BANDS` pins the surface (`secondary` / `muted` / `accent` lightness) and framing (`border` / `input` lightness) bands per mode so derived neutrals fit the chosen background.
import { RADIUS_PRESETS, COLOR_MODE_BANDS } from "@hex-core/tokens";
console.log(Object.keys(RADIUS_PRESETS)); // ["sharp", "balanced", "soft"]
console.log(COLOR_MODE_BANDS.light.surface); // 0.959Workflows
Swap themes at runtime in the studio canvas
Inject a `<style>` tag with `themeToScopedRuntimeCss` output. Change the input theme to repaint the scoped surface — no Tailwind rebuild required.
import { themeToScopedRuntimeCss, midnightTheme } from "@hex-core/tokens";
const css = themeToScopedRuntimeCss(midnightTheme, {
mode: "light",
scope: "[data-studio-canvas]",
});
// In a Server Component:
return <style>{css}</style>;Build a custom theme by extending an existing one
Themes are plain objects. Spread + override. The `Theme` type from `@hex-core/registry` is strict — TypeScript will tell you if you forget a token.
import { defaultTheme } from "@hex-core/tokens";
import type { Theme } from "@hex-core/registry";
export const brandTheme: Theme = {
...defaultTheme,
name: "brand",
tokens: {
...defaultTheme.tokens,
light: {
...defaultTheme.tokens.light,
primary: { value: "12 100% 50%", type: "color" },
},
},
};Compatibility
- Strict types against `@hex-core/registry@^0.3.2` (`Record<string, TokenValue>`, not `Record<string, unknown>`).
- Zero React or DOM dependencies — works in Node generators, Vite plugins, edge runtimes.
- Token values use HSL triplets (`<H> <S>% <L>%`), the format `<input type="color">` and the brand-native ColorPicker both accept.
See also
Verified against @hex-core/components@1.12.0 · @hex-core/tokens@1.3.6 · @hex-core/themes@0.2.2 · @hex-core/registry@0.5.1 · @hex-core/payload@0.3.0