npm.io
1.3.8 • Published yesterday

@hex-core/tokens

Licence
MIT
Version
1.3.8
Deps
3
Size
130 kB
Vulns
0
Weekly
0
Stars
3

@hex-core/tokens

npm downloads License: MIT

Design token engine for Hex Core — HSL color tokens + typography scale + shadow/radius tokens shared between components and themes.

Install

pnpm add @hex-core/tokens

Or, more commonly, copy the CSS block from the Theming guide directly into your app's globals.css.

Contents

  • Light + dark HSL palette (zinc neutral base, WCAG-safe pairs)
  • Typography scale (--text-2xs--text-5xl)
  • Radii (--radius-sm/md/lg)
  • Shadows (--shadow-glow)
  • Container widths

CSS variable namespaces

Tokens live in two namespaces depending on which layer is reading them. Knowing the contract avoids surprises when you author globals.css, override at runtime, or wire Tailwind v4.

--<key> — raw values, key-only namespace (source of truth)

themeToCss(theme) emits CSS custom properties using the raw value verbatim, with no hsl() wrapper and no --color- prefix. The exact shape of the value depends on the token type — colors are stored as H S L triplets in the bundled themes, radii as lengths, durations as times, etc. themeToCss doesn't transform the value; it just writes it under --<key>.

@layer base {
  :root {
    --background: 0 0% 100%;       /* color, stored as triplet */
    --foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --radius-md: 0.5rem;            /* length */
    --duration-fast: 150ms;         /* time */
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    /* … */
  }
}

Why store colors as triplets: alpha composition. hsl(var(--background) / 0.5) requires the var to hold 0 0% 100%, not hsl(0 0% 100%). Utilities like bg-background/50 and ring/border opacity overrides depend on this — wrapping at the source forfeits them.

themeToFlatJson() shares this namespace (returns Record<"--<key>", string> for AI/LLM consumption).

--color-<key> — Tailwind v4 @theme (consumption)

Tailwind v4's @theme directive demands a --color-<key> namespace to auto-generate utilities like bg-background, text-foreground, border-input. The docs site authors this layer by hand in apps/docs/src/app/globals.css:

@theme {
  --color-background: hsl(0 0% 100%);
  --color-foreground: hsl(240 10% 3.9%);
  --color-primary: hsl(240 5.9% 10%);
}

.dark {
  --color-background: hsl(240 10% 3.9%);
  /* … */
}

This shape has the hsl() wrapper and the --color- prefix — different from the raw layer.

If you want one source of truth plus Tailwind v4 utilities, declare the raw values at :root/.dark and delegate from @theme:

@theme {
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  /* … */
}

:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 3.9%;
}

.dark {
  --background: 240 10% 3.9%;
  --foreground: 0 0% 98%;
}

Now your runtime overrides (.theme-vivid { --background: 270 100% 95%; }) only need to touch the raw layer — Tailwind utilities follow automatically because @theme resolves var(--<key>) at paint time.

Use plain @theme, not @theme inline. The inline form substitutes the value at build time and would freeze the var(--<key>) lookup to whatever the variable resolved to during build, breaking the bridge. The non-color tokens in the docs site use @theme inline deliberately because they're static (radii, font sizes); colors stay in plain @theme so the cascade works.

The docs site itself doesn't bridge — it inlines hsl(...) directly in @theme and re-declares colors in .dark. That's intentional: the docs app never round-trips through themeToCss, so a separate raw layer would just be duplication. Bridge when you ship to v4 consumers AND you need them to override raw values in custom themes (.theme-foo { --background: ... }).

Runtime overrides — themeToScopedRuntimeCss

When an app needs to flip themes at runtime without round-tripping through globals.css (e.g. a theme studio's preview canvas, a per-org branding scope, a user-customized palette stored in URL state), use themeToScopedRuntimeCss(theme, { scope?, mode? }). It emits a single CSS rule that drops both namespaces in one call:

import { defaultTheme, themeToScopedRuntimeCss } from "@hex-core/tokens";

const css = themeToScopedRuntimeCss(defaultTheme, {
  scope: ".studio-canvas-active",
  mode: "light",
});
// →
// .studio-canvas-active {
//   --background: 0 0% 100%;
//   --color-background: hsl(0 0% 100%);
//   --primary: 240 5.9% 10%;
//   --color-primary: hsl(240 5.9% 10%);
//   --radius-md: 0.5rem;
//   /* … */
// }

Inject via a <style> tag and the override applies to every descendant of the scoped element — Tailwind utilities resolve through @theme, alpha-composition utilities (bg-background/50, border-border/30) resolve through the raw layer.

scope defaults to :root. mode defaults to "light"; pass "dark" to render the dark palette (typically combined with a .dark selector applied by your theme provider).

Non-color tokens (radii, durations, spacing, font sizes) only emit the raw --<key> form — they don't go through @theme's color machinery.

Tailwind v3 helper

themeToTailwindConfig(theme) returns the same Tailwind layer for the v3 config-file style:

import { defaultTheme, themeToTailwindConfig } from "@hex-core/tokens";

export default {
  theme: { extend: themeToTailwindConfig(defaultTheme) },
};

Output uses hsl(var(--<key>)) references (no --color- prefix needed in v3).

generateGlobalsCss is also Tailwind v3 — it wraps themeToCss output with @tailwind base/components/utilities directives. Don't use it inside a Tailwind v4 project.

Overriding

Every token is a CSS custom property. Override on :root or inside .dark for per-theme overrides. See the Theming guide for examples (accent hue swap, custom palette, typography scale).

Docs

hex-core.dev/docs/theming

License

MIT

Keywords