Themes that actually work

The index page shows @scope theme code alongside hardcoded Tailwind previews. The themes aren't doing anything. This page makes them real.

One shape's HTML never changes — only the theme wrapper changes — and you get genuinely different visual treatments. Not conceptually. With working CSS you can inspect.

Interface — UserProfile
interface UserProfile {
  name: string
  avatar: string
  role: "admin" | "member" | "guest"
  joinedAt: Date
}

The 3 shapes

Each shape is a fixed HTML structure that reads from CSS custom properties. No class names on children — @scope targets elements by type and position. The HTML is immutable per shape.

Horizontal — dashboard / sidebar
AW

Alan White

Admin
HTML — no class names
<div data-theme="light">
  <article data-shape="profile-horizontal">
    <span data-primitive="avatar" data-size="md">AW</span>
    <div>
      <h3>Alan White</h3>
      <span>Admin</span>
      <time data-primitive="timestamp" data-size="sm">January 2024</time>
    </div>
  </article>
</div>
Vertical — profile page
AW

Alan White

Admin
HTML — no class names
<div data-theme="light">
  <article data-shape="profile-vertical">
    <span data-primitive="avatar" data-size="lg">AW</span>
    <h3>Alan White</h3>
    <span data-primitive="badge">Admin</span>
    <time data-primitive="timestamp" data-size="md">Joined January 2024</time>
  </article>
</div>
Inline — inline / prose

Written by AW Alan White in the design channel.

HTML — no class names
<span data-shape="profile-inline">
  <span data-primitive="avatar" data-size="sm">AW</span>
  <span>Alan White</span>
</span>

Theme anatomy

A theme is a data-theme attribute that sets colour tokens as CSS custom properties. Every theme sets the same property names to different values. Themes are purely colour — shapes own all spacing, padding, layout, and radii.

data-theme="light"
[data-theme="light"] {
  --surface: #ffffff;
  --on-surface: #0a0a0a;
  --on-surface-muted: #525252;
  --on-surface-faint: #a3a3a3;
  --border: #e5e5e5;
  --accent: #6366f1;
  --avatar-bg: #e0e7ff;
  --avatar-text: #4338ca;
  --badge-bg: #f3f4f6;
  --badge-text: #374151;
}
data-theme="dark"
[data-theme="dark"] {
  --surface: #1e1e2e;
  --on-surface: #e2e2e2;
  --on-surface-muted: #a0a0b0;
  --on-surface-faint: #606070;
  --border: rgba(255,255,255,0.1);
  --accent: #818cf8;
  --avatar-bg: rgba(99,102,241,0.2);
  --avatar-text: #a5b4fc;
  --badge-bg: rgba(255,255,255,0.08);
  --badge-text: #c0c0d0;
}
@scope — Horizontal shape
@scope ([data-shape="profile-horizontal"]) {
  :scope {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.875rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
  }
  /* avatar owns its own appearance via
     @scope([data-primitive="avatar"]) */
  h3 { color: var(--on-surface); }
  span { color: var(--on-surface-muted); }
  /* timestamp owns its own appearance via
     @scope([data-primitive="timestamp"]) */
}

Same shape, swap the wrapper

The Horizontal shape below is identical in all four cases. Only the data-theme attribute on the wrapper changes. Inspect the DOM to verify — zero class names on shape elements.

data-theme="light"
AW

Alan White

Admin
data-theme="dark"
AW

Alan White

Admin
data-theme="brand"
AW

Alan White

Admin
data-theme="elevated"
AW

Alan White

Admin

The matrix — 3 shapes × 4 themes

3 shape definitions + 4 theme definitions = 12 distinct rendered outputs from 7 pieces of CSS. The traditional approach would require 12 bespoke component variants.

Every cell below is the same HTML per row. Only the wrapping data-theme attribute differs per column.

light
dark
brand
elevated
Horizontal
AW

Alan White

Admin
AW

Alan White

Admin
AW

Alan White

Admin
AW

Alan White

Admin
Vertical
AW

Alan White

Admin
AW

Alan White

Admin
AW

Alan White

Admin
AW

Alan White

Admin
Inline

Post by AW Alan White

Post by AW Alan White

Post by AW Alan White

Post by AW Alan White

Per-instance allocation

A realistic scenario: multiple UserProfile instances on the same page, each with a contextually appropriate shape and theme. Same interface data. Same shapes. Different themes per instance based on where they appear.

Sidebar team list — Horizontal + Dark
Team
AW

Alan White

Admin
SK

Sarah Kim

Member
MJ

Marcus Jones

Member
Featured member — Vertical + Brand
AW

Alan White

Admin
Article byline — Inline + Light

This post was written by AW Alan White and explores the separation of interface, shape, and theme in modern component architecture.

Reviewed by SKSarah Kim and MJMarcus Jones.

What just happened: Every instance above renders from the same UserProfile interface. The shapes (Horizontal, Vertical, Inline) are @scope blocks targeting semantic HTML by element type. The themes are data-theme attributes that set custom properties. No component was duplicated. No variant prop was needed. No class names on children. 3 shapes + 4 themes = 12 possible outputs from 7 definitions.

Design tokens — the forward path

The custom properties our themes set today map 1:1 to the W3C Design Tokens Community Group (DTCG) format. This isn't a future aspiration — the token spec reached stable status (v2025.10) and is already supported by Style Dictionary, Figma Variables, and Penpot.

Every --custom-property in our themes is a design token waiting for a tokens.json file. The mapping is direct.

tokens.json — DTCG format
{
  "surface":          { "$type": "color", "$value": "#ffffff" },
  "on-surface":       { "$type": "color", "$value": "#0a0a0a" },
  "on-surface-muted": { "$type": "color", "$value": "#525252" },
  "border":           { "$type": "color", "$value": "#e5e5e5" },
  "accent":           { "$type": "color", "$value": "#6366f1" },
  "avatar-bg":        { "$type": "color", "$value": "#e0e7ff" },
  "avatar-text":      { "$type": "color", "$value": "#4338ca" }
}
Generated CSS — what we already write
[data-theme="light"] {
  --surface: #ffffff;
  --on-surface: #0a0a0a;
  --on-surface-muted: #525252;
  --border: #e5e5e5;
  --accent: #6366f1;
  --avatar-bg: #e0e7ff;
  --avatar-text: #4338ca;
}

What this means: When you adopt the Intershapes model — themes as custom properties, shapes as @scope blocks — you're already aligned with the design tokens spec. Adding tooling (Style Dictionary to generate CSS from JSON, Figma to export tokens) is additive, not a migration. The CSS you write today is the token format, just authored by hand instead of generated.