> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Introducing Tier 3 Design Tokens

export const BlogMeta = ({date, author, tags}) => {
  const formatList = items => {
    if (!Array.isArray(items)) return items;
    if (items.length === 1) return items[0];
    if (items.length === 2) return `${items[0]} & ${items[1]}`;
    return `${items.slice(0, -1).join(", ")}, & ${items[items.length - 1]}`;
  };
  return <div className="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
      <span>{date}</span>
      <span>|</span>
      <span>{formatList(author)}</span>
      {tags && tags.length > 0 && <>
          <span>|</span>
          <div className="flex flex-wrap gap-1.5">
            {tags.map(tag => <span key={tag} className="px-2 py-1 rounded bg-gray-100 dark:bg-neutral-800 text-xs">
                {tag}
              </span>)}
          </div>
        </>}
    </div>;
};

<BlogMeta date="April 13, 2026" author="Ben Ho" tags={["Engineering", "Design", "New Features", "Web"]} />

Anvil2 3.0 ships **tier 3 component tokens**, completing our 3-tier design token architecture. If you use Anvil2 components out of the box, this requires no changes on your end — components already consume the new tokens internally. For teams that customize component styles or build custom themes, tier 3 tokens give you fine-grained control over individual component colors through a predictable cascade — override a semantic token and every component that references it updates automatically, or target a single component without affecting anything else.

This post walks through the architecture, shows how the cascade works with code examples, and covers what you need to know as a consumer.

## The 3-tier token architecture

Our token system is built on three layers, where each tier references the one below it:

```
Tier 1: Primitives       →  Raw values (hex colors, px sizes)
        ↑ referenced by
Tier 2: Semantic tokens   →  Purpose-driven (background, foreground, border)
        ↑ referenced by
Tier 3: Component tokens  →  Component-specific (button, checkbox, listbox)
```

### Tier 1 — Primitives

Raw, context-free design values. These are the foundation — color scales, spacing, radii, and typography values with no semantic meaning.

```json theme={null}
// color.tokens.json
{
  "color": {
    "blue": {
      "600": { "$type": "color", "$value": "#0265dc" },
      "700": { "$type": "color", "$value": "#1d4ca3" }
    }
  }
}
```

### Tier 2 — Semantic tokens

Purpose-driven tokens that reference primitives and include light/dark mode support. These answer the question "what is this color for?" rather than "what color is it?"

```json theme={null}
// background.tokens.json
{
  "background": {
    "color": {
      "primary": {
        "$type": "color",
        "$value": "{color.blue.600}",
        "$extensions": {
          "appearance": {
            "light": { "$type": "color", "$value": "{color.blue.600}" },
            "dark": { "$type": "color", "$value": "{color.blue.300}" }
          }
        }
      }
    }
  }
}
```

### Tier 3 — Component tokens (new in 3.0)

Component-specific tokens that reference semantic tokens. Each component's visual properties — foreground, background, and border colors across all variants and states — are defined through dedicated token files.

```json theme={null}
// component/button.tokens.json
{
  "button": {
    "primary": {
      "background": {
        "color":        { "$value": "{background.color.primary}" },
        "color-hover":  { "$value": "{background.color.primary-hover}" },
        "color-active": { "$value": "{background.color.primary-active}" }
      }
    }
  }
}
```

The full resolution chain for a button's primary background looks like this:

```
--a2-button-primary-background-color
  → references --a2-background-color-primary (tier 2)
    → references --a2-color-blue-600 (tier 1)
      → resolves to #0265dc
```

## How the theming cascade works

Tier 3 tokens enable two levels of customization:

### Bulk theming via tier 2 overrides

Override a semantic token and every component referencing it updates automatically. Change `--a2-background-color-primary` and Button, Chip, Tab, and any other component using that semantic meaning all pick up the new value.

### Targeted component overrides

Override a specific component token to customize just that component without touching anything else. Change `--a2-button-primary-background-color` and only Button is affected.

### Override method 1: CSS variable overrides

Set tier 2 semantic tokens directly as CSS custom properties via inline styles:

```tsx theme={null}
const cssVarOverrides: React.CSSProperties = {
  "--a2-background-color-primary": "light-dark(#9333ea, #a855f7)",
  "--a2-background-color-primary-hover": "light-dark(#7e22ce, #c084fc)",
  "--a2-background-color-primary-active": "light-dark(#6b21a8, #d8b4fe)",
};

<Flex style={cssVarOverrides}>
  <Button appearance="primary">Purple Button</Button>
  <Chip selected>Also purple</Chip>
</Flex>
```

### Override method 2: ThemeProvider (recommended)

Use the `ThemeProvider` component's `theme` prop for a type-safe, structured approach. ThemeProvider accepts `theme.semantic` and `theme.component` objects and handles light/dark mode values automatically:

```tsx theme={null}
import { ThemeProvider, type CustomThemeType } from "@servicetitan/anvil2";

const customTheme: CustomThemeType = {
  semantic: {
    BackgroundColorPrimary: {
      value: "#9333ea",
      extensions: { appearance: { dark: { value: "#a855f7" } } },
    },
    BackgroundColorPrimaryHover: {
      value: "#7e22ce",
      extensions: { appearance: { dark: { value: "#c084fc" } } },
    },
    BackgroundColorPrimaryActive: {
      value: "#6b21a8",
      extensions: { appearance: { dark: { value: "#d8b4fe" } } },
    },
  },
};

<ThemeProvider theme={customTheme}>
  <Button appearance="primary">Purple Button</Button>
  <Chip selected>Also purple</Chip>
</ThemeProvider>
```

<Note>
  We recommend using `ThemeProvider` for token overrides. It generates the correct CSS custom properties for you, handles light/dark mode automatically, and provides type safety through the `CustomThemeType` interface.
</Note>

## What tier 3 covers today

Tier 3 currently covers **color tokens only** — foreground, background, and border colors for each component's variants and interactive states (hover, active, disabled). All stable (non-beta) components now consume tier 3 tokens when defined. We ship 47 component token files with 3.0.

Each file follows the same predictable naming convention: `{component}.{variant}.{property}.{attribute}`. For example, `button.primary.background.color-hover` or `listbox.option.selected.foreground.color`.

## CSS variable cascading: what works and what doesn't

An important caveat about runtime CSS variable overrides. **Overriding a tier 1 primitive** (e.g., `--a2-color-blue-600`) **does not cascade** to tier 2 or tier 3 tokens at runtime.

This happens because the build process inserts the `light-dark()` CSS function at the semantic tier to support dark mode. The `light-dark()` function creates isolated branches that inline resolved values rather than preserving live `var()` references back to primitives.

```css theme={null}
/* You might expect a live chain: */
--a2-button-primary-background-color
  → var(--a2-background-color-primary)
    → var(--a2-color-blue-600)

/* But the built output inlines light-dark() at the semantic level: */
--a2-button-primary-background-color: var(--a2-background-color-primary,
  light-dark(var(--a2-color-blue-600, #0265dc), var(--a2-color-blue-300, #70b1ff)));
```

Because `light-dark()` resolves its branches independently, overriding `--a2-color-blue-600` at runtime does not propagate upward.

**What does work:**

* **Override tier 2 (semantic):** Set `--a2-background-color-primary` to affect all components referencing that semantic token
* **Override tier 3 (component):** Set `--a2-button-primary-background-color` to customize a single component

**Our recommendation:** Use `ThemeProvider` for overrides. It accepts `theme.semantic` and `theme.component` objects that correctly generate the CSS variable overrides for you, handling light/dark mode values automatically. This is the safest and most reliable way to customize token values.

<Warning>
  This tradeoff exists because `light-dark()` insertion is necessary for dark mode support. The current design intentionally optimizes for dark mode correctness. Override at tier 2 or tier 3 — not tier 1.
</Warning>

## What's coming next

This release establishes the tier 3 architecture for colors. In future releases, we plan to expand tier 3 to cover additional properties:

* **Border radius** — component-specific corner rounding
* **Padding and spacing** — internal component spacing
* **Other visual properties** — as the system matures

This will give teams even more granular control over component styling through the same predictable token cascade.

## Breaking changes to tier 2 tokens

If you use Anvil2 components out of the box without custom token overrides, these changes do not affect you — components already consume the updated tokens internally. However, if you reference tier 2 tokens directly in custom components or overrides, review these changes:

**Overlay tokens removed — interactive states now use direct background colors.** In 2.x, interactive states (hover, active) were implemented using a pseudo-element with an overlay color layered on top of the base background. In 3.0, each background color token now defines its own interactive state variants directly (e.g., `background.color.primary-hover`, `background.color.primary-active`). This eliminates the need for the `overlay` token category entirely:

| 2.x                            | 3.0                                           |
| ------------------------------ | --------------------------------------------- |
| `overlay.color.hover.default`  | `background.color.transparent.default-hover`  |
| `overlay.color.active.default` | `background.color.transparent.default-active` |
| `overlay.color.hover.primary`  | `background.color.transparent.primary-hover`  |

**`$root` pattern removed.** Interactive states are now flat hyphenated siblings instead of nested children:

```
// 2.x
foreground.color.primary         (was $root)
foreground.color.primary.hover
foreground.color.primary.active

// 3.0
foreground.color.primary
foreground.color.primary-hover
foreground.color.primary-active
```

<Note>
  For the full migration guide, see [Migrating from 2.0 to 3.0](/docs/resources/migration-guides/2.0-to-3.0).
</Note>

***

Questions, feedback, or ideas? Drop us a line in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS).
