Skip to main content

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.

Anvil2 supports theming so apps can reflect product branding and react to user mode preferences. This page covers how the token cascade works and where to apply overrides.

How the token cascade works

Anvil2 tokens are organized in three tiers: primitives (raw values), semantic tokens (values with purpose — e.g. background.color.primary), and component tokens (values scoped to one component — e.g. button.primary.background.color). Each tier references the tier below it: component tokens reference semantic tokens, and semantic tokens reference primitives. Tiers are never skipped. Overriding a token fans out to everything that references it. Override background.color.primary and every component that consumes it — Button, Alert, Badge, and many more — updates together. Override button.primary.background.color and only Button changes. Primitive overrides do not cascade at runtime; see Cascading limitations below.

Overriding at the semantic level (tier 2)

Use this for bulk theming — reshaping the design system to match a product brand. The standard way is ThemeProvider:
import { ThemeProvider } from "@servicetitan/anvil2";

<ThemeProvider
  theme={{
    semantic: {
      BackgroundColorPrimary: {
        value: "#7c3aed",
        extensions: { appearance: { dark: { value: "#a78bfa" } } },
      },
    },
  }}
>
  {/* app tree */}
</ThemeProvider>;
The equivalent CSS variable override:
:root {
  --a2-background-color-primary: light-dark(#7c3aed, #a78bfa);
}
Semantic categories you can theme: foreground, background, border, status, focus. See the full inventory on Design Tokens.

Overriding at the component level (tier 3)

Use this to customize a single component without touching anything else. Same shape as semantic overrides, with a component object:
<ThemeProvider
  theme={{
    component: {
      ButtonPrimaryBackgroundColor: {
        value: "#0ea5e9",
        extensions: { appearance: { dark: { value: "#38bdf8" } } },
      },
    },
  }}
>
  {/* subtree */}
</ThemeProvider>;
Or as a CSS variable:
.my-panel {
  --a2-button-primary-background-color: light-dark(#0ea5e9, #38bdf8);
}
Every component has its own tokens page listing its full token set — see any component’s /tokens tab.

Cascading limitations

Tier-1 primitive overrides don’t cascade at runtime. Component and semantic tokens embed the resolved primitive value at build time (via light-dark()), so changing ColorBlue500 through ThemeProvider has no effect on BackgroundColorPrimary or ButtonPrimaryBackgroundColor. Override at tier 2 or tier 3 instead. Prefer ThemeProvider over raw CSS variables. ThemeProvider kebabizes keys, prefixes with --a2-, and wraps light/dark values in light-dark() automatically. A raw CSS override has to do all three manually and drops dark mode support if only a light value is supplied.

Dark mode

Design

Anvil2 Figma assets use variables for light and dark mode. Refer to Figma’s guidance on how to switch between modes.

Code

Toggle app-wide mode via AnvilProvider:
import { AnvilProvider } from "@servicetitan/anvil2";

return (
  <AnvilProvider themeData={{ mode: "dark" }}>
    <Button>This is a Button</Button>
  </AnvilProvider>
);
AnvilProvider includes a ThemeProvider under the hood. To change the theme of part of a page without affecting the rest, use ThemeProvider directly:

How light-dark() works under the hood

Every themed CSS variable is authored as light-dark(<light>, <dark>). The ThemeProvider component flips which branch is live by applying a .mode-light or .mode-dark class that sets color-scheme: light or dark on the subtree:
:root {
  --a2-background-color: light-dark(#ffffff, #141414);
}
.mode-dark {
  color-scheme: dark;
}
.mode-light {
  color-scheme: light;
}
The browser resolves light-dark() against color-scheme, so toggling the class is enough — no JavaScript re-render of styles is needed.

How ThemeProvider picks up dark values automatically

When you override a token without supplying a dark value, ThemeProvider falls back to the base token’s extensions.appearance.dark.value for the dark branch. In practice this means you can write a single-value light override and dark mode keeps working.
  • Design Tokens — complete tier and category reference.
  • Each component’s /tokens page lists its tier-3 tokens and override examples.
Last modified on April 21, 2026