Skip to content

Latest commit

 

History

History
392 lines (324 loc) · 9.53 KB

File metadata and controls

392 lines (324 loc) · 9.53 KB

CSS Patterns: Composing Layers on tokenctl Output

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.

Table of Contents

  1. The Composition Pattern
  2. Accessibility Media Queries
  3. Typography Systems
  4. Interaction States

The Composition Pattern

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

Accessibility Media Queries

Define Base Tokens

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"
    }
  }
}

Accessibility Layer

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);
  }
}

Usage in Components

.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 Systems

Typography tokens work with existing tokenctl types. Define tokens, then compose text styles in CSS.

Define Typography Tokens

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" }
    }
  }
}

Typography Composition Layer

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);
}

Fluid Typography

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);
}

Interaction States

Define base tokens, compose state variants in CSS.

Define Base Tokens

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" }
  }
}

Interaction States Layer

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;
}

Button Example

.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;
}

Summary

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.