tokenctl generates CSS custom properties from your design tokens. This document covers CSS patterns for building on top of that output — accessibility, typography, interaction states, and project structure.
For tokenctl features themselves (token types, expressions, @property, components), see TOKENS.md.
tokenctl generates tokens.css. You layer additional CSS on top:
my-system/
├── tokens/
│ └── *.json # Your token definitions
├── dist/
│ └── tokens.css # Generated by tokenctl
├── layers/
│ ├── accessibility.css # Media query overrides
│ ├── typography.css # Font compositions
│ └── states.css # Interaction patterns
└── app.css # Final composition
app.css:
/* 1. Tailwind base */
@import "tailwindcss";
/* 2. tokenctl generated tokens */
@import "./dist/tokens.css";
/* 3. Your custom layers */
@import "./layers/accessibility.css";
@import "./layers/typography.css";
@import "./layers/states.css";This separation keeps:
- Tokens as data (JSON) — portable, tool-agnostic
- CSS behavior as CSS — full control, no abstraction
tokens/motion.json:
{
"motion": {
"$type": "number",
"scale": {
"$value": 1,
"$description": "Multiplier for motion. Set to 0 for reduced motion."
}
},
"timing": {
"$type": "dimension",
"fast": { "$value": "150ms" },
"normal": { "$value": "250ms" },
"slow": { "$value": "400ms" }
}
}tokens/colors.json:
{
"color": {
"$type": "color",
"primary": { "$value": "oklch(50% 0.2 250)" },
"primary-high-contrast": {
"$value": "oklch(30% 0.3 250)",
"$description": "Higher contrast variant for prefers-contrast: more"
}
}
}layers/accessibility.css:
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
:root {
--motion-scale: 0;
--timing-fast: 0ms;
--timing-normal: 0ms;
--timing-slow: 0ms;
}
}
/* High Contrast */
@media (prefers-contrast: more) {
:root {
--color-primary: var(--color-primary-high-contrast);
}
}
/* Reduced Transparency */
@media (prefers-reduced-transparency: reduce) {
:root {
--opacity-overlay: 1;
--opacity-disabled: 0.7;
}
}
/* Dark Mode (if not using data-theme) */
@media (prefers-color-scheme: dark) {
:root {
--color-base-100: oklch(20% 0.02 250);
--color-base-content: oklch(90% 0.02 250);
}
}.btn {
transition: transform calc(var(--timing-fast) * var(--motion-scale)),
background-color var(--timing-fast);
}
.btn:hover {
transform: scale(calc(1 + (0.02 * var(--motion-scale))));
}When --motion-scale is 0, transitions are instant and transforms are disabled.
Typography tokens work with existing tokenctl types. Define tokens, then compose text styles in CSS.
tokens/typography.json:
{
"font": {
"family": {
"$type": "fontFamily",
"sans": { "$value": ["Inter", "ui-sans-serif", "system-ui", "sans-serif"] },
"serif": { "$value": ["Merriweather", "ui-serif", "Georgia", "serif"] },
"mono": { "$value": ["JetBrains Mono", "ui-monospace", "monospace"] },
"display": { "$value": ["Inter Display", "Inter", "system-ui", "sans-serif"] }
},
"weight": {
"$type": "number",
"light": { "$value": 300 },
"normal": { "$value": 400 },
"medium": { "$value": 500 },
"semibold": { "$value": 600 },
"bold": { "$value": 700 }
},
"size": {
"$type": "dimension",
"xs": { "$value": "0.75rem" },
"sm": { "$value": "0.875rem" },
"base": { "$value": "1rem" },
"lg": { "$value": "1.125rem" },
"xl": { "$value": "1.25rem" },
"2xl": { "$value": "1.5rem" },
"3xl": { "$value": "1.875rem" },
"4xl": { "$value": "2.25rem" },
"5xl": { "$value": "3rem" }
},
"leading": {
"$type": "number",
"$description": "Line heights as unitless multipliers",
"tight": { "$value": 1.25 },
"snug": { "$value": 1.375 },
"normal": { "$value": 1.5 },
"relaxed": { "$value": 1.625 }
},
"tracking": {
"$type": "dimension",
"$description": "Letter spacing",
"tight": { "$value": "-0.025em" },
"normal": { "$value": "0em" },
"wide": { "$value": "0.025em" }
}
}
}layers/typography.css:
.text-body {
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-normal);
line-height: var(--font-leading-normal);
}
.heading-1 {
font-family: var(--font-family-display);
font-size: var(--font-size-5xl);
font-weight: var(--font-weight-bold);
line-height: var(--font-leading-tight);
letter-spacing: var(--font-tracking-tight);
}
.heading-2 {
font-family: var(--font-family-display);
font-size: var(--font-size-4xl);
font-weight: var(--font-weight-bold);
line-height: var(--font-leading-tight);
}
.heading-3 {
font-family: var(--font-family-sans);
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-semibold);
line-height: var(--font-leading-snug);
}
.text-caption {
font-family: var(--font-family-sans);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: var(--font-leading-normal);
letter-spacing: var(--font-tracking-wide);
}
.text-code {
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
line-height: var(--font-leading-relaxed);
}For responsive type that scales with viewport:
layers/fluid-type.css:
:root {
/* Fluid base size: 16px at 320px viewport, 20px at 1280px */
--font-size-fluid-base: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
/* Scale other sizes relative to fluid base */
--font-size-fluid-sm: calc(var(--font-size-fluid-base) * 0.875);
--font-size-fluid-lg: calc(var(--font-size-fluid-base) * 1.125);
--font-size-fluid-xl: calc(var(--font-size-fluid-base) * 1.25);
--font-size-fluid-2xl: calc(var(--font-size-fluid-base) * 1.5);
}Define base tokens, compose state variants in CSS.
tokens/interactions.json:
{
"state": {
"$type": "number",
"hover-opacity": { "$value": 0.9 },
"active-scale": { "$value": 0.98 },
"disabled-opacity": { "$value": 0.5 },
"focus-ring-width": { "$value": 2, "$description": "px" }
},
"timing": {
"$type": "dimension",
"hover": { "$value": "150ms" },
"active": { "$value": "75ms" },
"focus": { "$value": "100ms" }
}
}layers/states.css:
/*
* State variant generation using relative color syntax (oklch)
* Browser support: Chrome 111+, Safari 16.4+, Firefox 128+
*/
:root {
/* Hover: slightly darker */
--color-primary-hover: oklch(from var(--color-primary) calc(l * 0.9) c h);
--color-secondary-hover: oklch(from var(--color-secondary) calc(l * 0.9) c h);
/* Active: more darker */
--color-primary-active: oklch(from var(--color-primary) calc(l * 0.8) c h);
/* Disabled: reduced opacity */
--color-primary-disabled: oklch(from var(--color-primary) l c h / var(--state-disabled-opacity));
}
/* Reusable focus ring */
.focus-ring {
outline: var(--state-focus-ring-width) solid transparent;
outline-offset: 2px;
transition: outline-color var(--timing-focus);
}
.focus-ring:focus-visible {
outline-color: var(--color-primary);
}
/* Interactive element base */
.interactive {
transition:
background-color var(--timing-hover),
transform var(--timing-active),
opacity var(--timing-hover);
}
.interactive:hover {
opacity: var(--state-hover-opacity);
}
.interactive:active {
transform: scale(var(--state-active-scale));
}
.interactive:disabled {
opacity: var(--state-disabled-opacity);
cursor: not-allowed;
}.btn {
background-color: var(--color-primary);
color: var(--color-primary-content);
transition:
background-color var(--timing-hover),
transform var(--timing-active);
}
.btn:hover:not(:disabled) {
background-color: var(--color-primary-hover);
}
.btn:active:not(:disabled) {
background-color: var(--color-primary-active);
transform: scale(var(--state-active-scale));
}
.btn:disabled {
background-color: var(--color-primary-disabled);
cursor: not-allowed;
}
.btn:focus-visible {
outline: var(--state-focus-ring-width) solid var(--color-primary);
outline-offset: 2px;
}| Pattern | Where to Define | Why |
|---|---|---|
| Token values | tokens/*.json |
Data, portable, tool-agnostic |
| @property | tokens/*.json with $property: true |
Auto-generated from token data |
| Media queries | layers/accessibility.css |
CSS behavior, full control |
| Text styles | layers/typography.css |
Composed from token vars |
| State variants | layers/states.css |
CSS-native relative colors |
tokenctl handles tokens and @property declarations. CSS handles behavior. This separation keeps your design system maintainable and your tokens portable.