Codeskill

Learn to code, step by step

Mini project: themeable component library (CSS-only API)

This mini project builds a small themeable component library – button, card, alert, and input – using design tokens, cascade layers, and a CSS-only customisation API. No preprocessor required. Optional JavaScript for a theme toggle only.

What you are building

  • Layered CSS: tokens, base, components, utilities.
  • Light and dark themes via data-theme on <html>.
  • Four components styled only with semantic custom properties.
  • Consumer override API: data-variant, data-size, and token overrides on a wrapper.
  • One demo page exercising all states.

File structure

css/
  tokens.css
  base.css
  components/
    button.css
    card.css
    alert.css
    input.css
  utilities.css
  main.css
index.html

Layer entry file

/* main.css */
@layer tokens, base, components, utilities;

@import url("tokens.css") layer(tokens);
@import url("base.css") layer(base);
@import url("components/button.css") layer(components);
@import url("components/card.css") layer(components);
@import url("components/alert.css") layer(components);
@import url("components/input.css") layer(components);
@import url("utilities.css") layer(utilities);

Tokens and themes

/* tokens.css */
@layer tokens {
  :root {
    --font-sans: system-ui, sans-serif;
    --radius-sm: 0.25rem;
    --radius-md: 0.5rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    color-scheme: light dark;
  }

  [data-theme="light"] {
    --color-brand: oklch(0.55 0.18 250);
    --color-brand-on: oklch(0.99 0.02 250);
    --color-surface: oklch(0.99 0.005 250);
    --color-text: oklch(0.2 0.02 250);
    --color-border: oklch(0.88 0.01 250);
    --color-danger: oklch(0.55 0.2 25);
    --color-success: oklch(0.55 0.15 145);
  }

  [data-theme="dark"] {
    --color-brand: oklch(0.65 0.16 250);
    --color-brand-on: oklch(0.15 0.02 250);
    --color-surface: oklch(0.2 0.02 250);
    --color-text: oklch(0.95 0.01 250);
    --color-border: oklch(0.35 0.02 250);
    --color-danger: oklch(0.65 0.18 25);
    --color-success: oklch(0.65 0.14 145);
  }
}

Button component

@layer components {
  :where(.cs-btn) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    font: 600 0.9375rem / 1.2 var(--font-sans);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    cursor: pointer;
    background: var(--btn-bg, var(--color-brand));
    color: var(--btn-fg, var(--color-brand-on));
  }

  :where(.cs-btn):focus-visible {
    outline: 2px solid var(--color-brand);
    outline-offset: 2px;
  }

  :where(.cs-btn[data-variant="ghost"]) {
    --btn-bg: transparent;
    --btn-fg: var(--color-brand);
    border-color: var(--color-border);
  }

  :where(.cs-btn[data-size="sm"]) {
    padding: var(--space-2) var(--space-3);
    font-size: 0.8125rem;
  }
}

CSS-only consumer API

Consumers customise without editing component files:

/* brand portal wrapper */
.portal-acme {
  --color-brand: oklch(0.5 0.2 30);
  --color-brand-on: oklch(0.99 0.02 30);
}

/* one-off button override */
.checkout-bar {
  --btn-bg: var(--color-success);
  --btn-fg: white;
}

Semantic component variables (--btn-bg) accept token overrides from any ancestor. No modifier class explosion.

Alert and card patterns

:where(.cs-alert) {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--alert-border, var(--color-border));
  background: var(--alert-bg, var(--color-surface));
  color: var(--color-text);
}

:where(.cs-alert[data-variant="danger"]) {
  --alert-border: var(--color-danger);
  --alert-bg: oklch(from var(--color-danger) l c h / 0.12);
}

:where(.cs-card) {
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

Acceptance checklist

  • Toggle data-theme on <html> switches all components.
  • Wrapper class overrides brand colour for every child component.
  • Focus-visible rings visible on buttons and inputs.
  • prefers-reduced-motion respected if you add transitions.
  • No unlayered rules outside the declared stack.

That completes Going deep with CSS. You should be able to architect layered systems, ship performant motion, handle modern colour and typography, deliver CSS efficiently, choose tooling with eyes open, catch regressions early, and publish a small library others can theme without forking your files.

PreviousTesting visual regressions lightly