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

# Theming

> How to override Anvil2 tokens for bulk theming or single-component customization, apply the Carto theme, and use dark mode.

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](#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`:

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

<ThemeProvider
  theme={{
    semantic: {
      BackgroundColorPrimary: {
        value: "#7c3aed",
        extensions: { appearance: { dark: { value: "#a78bfa" } } },
      },
    },
  }}
>
  {/* app tree */}
</ThemeProvider>;
```

The equivalent CSS variable override:

```css theme={null}
:root {
  --a2-background-color-primary: light-dark(#7c3aed, #a78bfa);
}
```

Semantic categories you can theme include **foreground**, **background**, **border**, **typography**, **status**, and **focus**. See the full inventory on [Design Tokens](https://anvil.servicetitan.com/docs/design-tokens.md).

## 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:

```tsx theme={null}
<ThemeProvider
  theme={{
    component: {
      ButtonPrimaryBackgroundColor: {
        value: "#0ea5e9",
        extensions: { appearance: { dark: { value: "#38bdf8" } } },
      },
    },
  }}
>
  {/* subtree */}
</ThemeProvider>
```

Or as a CSS variable:

```css theme={null}
.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.

## Using the Carto theme

> [!NOTE]
>
> Use the `carto` theme only in product areas approved to use Carto styling to
> match the [AI Kit](https://anvil.servicetitan.com/docs/building-agentic-experiences). Do not apply it to general Anvil2
> surfaces.

`@servicetitan/anvil2/themes` exports a ready-made `carto` theme that maps Carto radius and typography onto Anvil2 semantic tokens. Pass it to `ThemeProvider` when an Anvil2 surface should align with Carto.

```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
import { carto } from "@servicetitan/anvil2/themes";

<ThemeProvider theme={carto}>{/* app tree */}</ThemeProvider>;
```

What `carto` sets:

| Area | Mapping |
| - | - |
| Border radius | Carto `sharp` / `100` / `trim` / `moderate` / `generous` / `full` → Anvil2 `BorderRadiusNone` through `BorderRadiusCircular` (buttons and inputs use `trim` via `BorderRadiusMedium`) |
| Paragraph and label fonts | Inter (`typography.fontFamily.functional`) |
| Heading font | Sofia Pro (`typography.fontFamily.expressive`) |
| Weights | Paragraph `400`; semibold primitive (`FontWeightSemibold`) `500`; bold primitive (`FontWeightBold`) `600`; heading `700` |

### Load the Inter font

Load Inter yourself (for example from Google Fonts). `AnvilProvider` already loads Sofia Pro through `@servicetitan/anvil-fonts`.

```html theme={null}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css?family=Inter:400,400i,500,500i,700,700i&display=swap"
  rel="stylesheet"
/>
```

## Dark mode

### Design

Anvil2 Figma assets use variables for light and dark mode. Refer to [Figma's guidance on how to switch between modes](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables).

### Code

Toggle app-wide mode via `AnvilProvider`:

```tsx theme={null}
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:

theming-playground

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/theming-playground?file=src/App.tsx)

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

function App() {
  return (
    <Card gap="2">
      This card will default to the page&apos;s theme.
      <ThemeProvider mode="dark">
        <Card>
          This card will change color based on the ThemeProvider.mode!
        </Card>
      </ThemeProvider>
    </Card>
  );
}

export default App;
```

### 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:

```css theme={null}
: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.

## Related

* [Design Tokens](https://anvil.servicetitan.com/docs/design-tokens.md) — complete tier and category reference.
* Each component's `/tokens` page lists its tier-3 tokens and override examples.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.