The design-token CONTRACT — author one TokenSpec, project it into CSS variables, the Tailwind v4 @theme block, and a shadcn token map.
CANDIDATE tooling — not official OpenAPI. Part of the Suluk OpenAPI v4.0 candidate, not a SIG deliverable. Apache-2.0.
bun add @suluk/theme
TokenSpec (OKLCH colors + radius/fonts/shadows + type/spacing/breakpoint scales) projects into three otherwise hand-synced surfaces: a :root CSS-vars block, the Tailwind v4 @theme inline block, and a shadcn token map. They can't drift, because they're all derived from the same spec.Oklch value (l/c/h/alpha) with parse/format/clamp helpers. OKLCH keeps lightness independent of hue/chroma, which is what makes the dark-mode derivation tractable.deriveDark is a role-aware lightness remap that preserves each token's chroma + hue (surfaces go dark, text goes light, brand colors stay vivid). Same light spec in → same dark spec out.parseShadcnCss reads a shadcn / tweakcn theme CSS block into ColorTokens, so any community theme drops into the same emit/derive pipeline as a hand-authored spec.renderBaseCss emits a scheme-independent contract — :focus-visible rings, the [aria-invalid] error look, .sr-only/.skip-link, and a few motion primitives — all prefers-reduced-motion-gated.ColorTokens (e.g. to remap onto your own --bg/--accent var vocabulary, or to generate swatch previews).It is not a theme catalog or a runtime picker: this package ships only the contract + three reference schemes that prove the mechanism. The curated 40+ scheme catalog and the no-flash stamper/picker are app-layer breadth (see saasuluk's src/themes/*), built on top of this same TokenSpec.
import {
oklch, terracotta, themeFromLight,
toCssVars, toThemeCss, toTailwindTheme, toShadcnTokens,
type TokenSpec,
} from "@suluk/theme";
// Use a reference scheme, or author your own TokenSpec from OKLCH colors + a radius.
const theme = themeFromLight(terracotta); // { light, dark } — dark is derived if not supplied
toCssVars(terracotta); // ":root { --background: oklch(1 0 0); … --radius: 0.625rem; }"
toThemeCss(theme); // light at :root + dark at [data-theme='dark']
toTailwindTheme(terracotta); // "@theme inline { --color-background: var(--background); --radius-lg: var(--radius); … }"
toShadcnTokens(terracotta); // { "--primary": "oklch(0.6397 0.172 36.44)", "--radius": "0.625rem", … }
toCssVars takes an optional { selector } and toThemeCss an optional { darkSelector } (default [data-theme='dark']).
import { deriveDark, graphite } from "@suluk/theme";
const dark = deriveDark(graphite); // surfaces darken, every *foreground role goes light, brand stays vivid
// deriveDark is pure: deriveDark(graphite) deep-equals deriveDark(graphite)
import { parseShadcnCss, formatOklch, withLightness, type ColorTokens } from "@suluk/theme";
const css = `:root {
--background: oklch(0.99 0 0); --foreground: oklch(0.1 0 0);
--primary: oklch(0.55 0.2 250); --primary-foreground: oklch(1 0 0);
--muted: oklch(0.97 0 0); --border: oklch(0.92 0 0); --ring: oklch(0.55 0.2 250);
}`;
const t = parseShadcnCss(css, "demo");
// → { name: "demo", light: ColorTokens, dark: ColorTokens } | null
// dark is read from a `.dark` / html[data-theme="dark"] block when present, else derived.
// Omitted tokens are filled coherently; null only when the :root color essentials are absent.
if (t) {
// remap the shadcn roles onto your own var vocabulary (hue-preserving tints stay coherent):
const accent2 = withLightness(t.light.primary, t.light.primary.l + 0.07);
const bg = formatOklch(t.light.background); // "oklch(0.99 0 0)"
}
import { oklch, parseOklch, formatOklch, withLightness, withAlpha } from "@suluk/theme";
oklch(0.64, 0.172, 36.44); // { l, c, h } — clamped to valid ranges
formatOklch(oklch(0.6, 0.1, 200, 0.5)); // "oklch(0.6 0.1 200 / 0.5)"
parseOklch("oklch(50% 0.1 120)"); // { l: 0.5, c: 0.1, h: 120 } (percent L normalized)
parseOklch("rgb(1,2,3)"); // null
import { renderBaseCss } from "@suluk/theme";
renderBaseCss(); // defaults to the shadcn role vars (var(--ring), var(--destructive))
renderBaseCss({ ring: "var(--accent)", destructive: "#ef4444" }); // point it at your own var vocabulary
Emit it once into your global stylesheet. It owns the keyboard focus ring, the invalid-field look, the skip-link, and the motion keyframes — all neutralized under prefers-reduced-motion.
| Export | What it does |
|---|---|
oklch, clampOklch, formatOklch, parseOklch, withLightness, withAlpha, Oklch |
the OKLCH value type + parse/format/clamp helpers |
TokenSpec, ThemeSpec, ColorTokens, FontTokens |
the design-token contract types |
COLOR_ROLES, cssVarName |
the ordered color-role list + role → CSS-var name (primaryForeground → --primary-foreground) |
deriveDark, themeFromLight |
deterministic dark-from-light derivation; build a { light, dark } spec |
toCssVars, toThemeCss, toTailwindTheme, toShadcnTokens, renderBaseCss |
the projections (+ CssVarsOptions, ThemeCssOptions, BaseCssOptions) |
parseShadcnCss |
import a shadcn/tweakcn theme CSS block into ColorTokens |
graphite, terracotta, ocean, REFERENCE_SCHEMES |
three reference schemes that prove the mechanism end-to-end |
This package is L3 — it generates / projects, it does not host. It hands you CSS strings, a Tailwind block, and a token map that you own and emit; there is no runtime, no theme server, no bundled picker. The curated scheme catalog, the no-flash <html> stamper, and the runtime picker are app-layer breadth (they live in the consuming app, e.g. saasuluk's src/themes/) and are authored on top of this same TokenSpec. The single entry point is @suluk/theme — no CLI and no sub-path exports.
Apache-2.0