> ## 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 2.0 to 3.0

## Anvil2 3.0 Release

Anvil2 3.0 is a **CSS-only** breaking release. We've made some foundational changes to our CSS structure to address critical issues we've faced in the first two versions of Anvil2, and to provide a more robust tokens and colors system. This page will describe the changes introduced in 3.0, as well as the things that product teams need to test.

The scope of this change also requires that **all monolith and MFE projects will need to upgrade to 3.0** in the `78Vega` release. Anvil2 3.0 will not be compatible with earlier versions. Due to this requirement, we've decided to hold off on any non-CSS breaking changes in this version.

***

<Tabs>
  <Tab title="Designer">
    ## Anvil2 v3.0 migration guide for designers

    Anvil2 v3.0 is available now for designers working on 78Vega. This guide covers what's changing with tokens and how to set up your Figma workspace. The full release is targeted for June 6, 2026.

    If you're not working on 78Vega, wait for the official release.

    ***

    ## What's new in v3.0

    ### Tokens

    Anvil2 v3.0 introduces changes across all three token tiers.

    #### Tier 3 — New

    Tier 3 tokens are a new addition to the system. They represent component-level semantic decisions — things like surface color, border, and interactive states. You won't use tier 3 tokens directly in your designs. Components in the Anvil2 v3.0 library are already built with them, so the work is handled automatically when you use updated components.

    #### Tier 2 — Name changes

    Some tier 2 tokens have minor name updates for consistency. If you reference tier 2 tokens directly in your designs, review any custom components or local styles that use them and update as needed.

    ##### Overlay → Transparent Background

    The entire `overlay` token category was removed and replaced with `background.color.transparent.*`.

    | 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`  |
    | `overlay.color.active.primary` | `background.color.transparent.primary-active` |
    | `overlay.color.hover.danger`   | `background.color.transparent.danger-hover`   |
    | `overlay.color.active.danger`  | `background.color.transparent.danger-active`  |

    ##### `$root` flattening (structural rename)

    Interactive state tokens were restructured from nested `$root` objects to flat hyphenated siblings. This pattern applies across all semantic token groups — `foreground`, `background`, `border`, etc. The `-hover` and `-active` suffixes are now flat siblings instead of nested children.

    **2.x:**

    ```text theme={null}
    foreground.color.primary        (was $root)
    foreground.color.primary.hover
    foreground.color.primary.active
    ```

    **3.0:**

    ```text theme={null}
    foreground.color.primary
    foreground.color.primary-hover
    foreground.color.primary-active
    ```

    #### Tier 1 — Expanded color ramp

    The tier 1 color ramp has been expanded with new values. Update adds coverage at the edges of the scale.

    ### Components

    Components in Anvil2 v3.0 have been rebuilt to match code props as closely as possible. This alignment reduces the gap between design and engineering, making handoff more predictable and consistent.

    Anvil2 v3.0 components also now support Figma Slots, allowing you to nest components inside other components while preserving their properties and behavior.

    ***

    ## Set up your Figma libraries

    The new Anvil2 libraries are not enabled by default. Follow these steps to enable them manually.

    ### Turn on the new libraries

    1. Open your Figma file.
    2. Go to **Main menu → Libraries**.
    3. Search for and enable:
       * **Anvil2 v3** *(replaces Anvil2)*
       * **Anvil2 Design Token**

    ### Turn off the old library

    4. In the same Libraries panel, locate **Anvil2** and toggle it off.

    <Note>
      Disabling the old library may cause components that haven't been swapped yet to show as unresolved. Complete the library swap before sharing files with others or handing off for review.
    </Note>

    ***

    ## Introducing Figma Slots

    Figma Slots let you define flexible areas within a component where other components can be inserted without detaching or overriding the parent's structure. Instead of hard-coding every possible content variation, you designate a region as a slot — any component dropped into that slot inherits the parent's layout constraints while keeping its own properties and variants intact.

    Slots make design system components more composable and reduce the need for one-off variants. Anvil2 v3.0 components are built with slot support, so you can nest and swap content freely within the updated library.

    **Learn more:**

    * [Use slots to build flexible components in Figma](https://help.figma.com/hc/en-us/articles/38231200344599-Use-slots-to-build-flexible-components-in-Figma)
    * [Supercharge your design system with slots](https://www.figma.com/blog/supercharge-your-design-system-with-slots/)

    ### Known limitation: Slots and variable properties

    Figma has a current limitation with slots and component variables. When a component is placed inside a slot, its layers become detached from the original component structure. As a result, component properties — including variant-based changes — may not update as expected when switching states.

    This is a known Figma issue the team is actively working to clarify. In the meantime, if a component inside a slot isn't responding to property changes, swap it out manually rather than relying on the variable to update it.

    [Read the Figma forum thread for full details.](https://forum.figma.com/report-a-problem-6/figma-slots-not-adapting-variable-properties-51771?tid=51771\&fid=6)

    ***

    ## Release timeline

    | Milestone                       | Date              |
    | ------------------------------- | ----------------- |
    | Anvil2 v3.0 release             | End of April 2026 |
    | Schema freeze                   | June 1, 2026      |
    | Feature complete / full release | June 6, 2026      |

    ***

    ## Next steps

    The Anvil team will enable Anvil2 v3.0 for all designers by default with the 78Vega release on June 6. When this happens, the old Anvil2 library will be switched off automatically — but not deleted. Existing spaces that aren't upgrading yet will retain access to the old library.

    Questions or issues? Reach out in #anvil-design-system or file an issue in the Anvil backlog.
  </Tab>

  <Tab title="Engineer">
    ## Migrating from Anvil2 2.x to 3.0

    <Note>
      **Anvil2 3.0 is scheduled for official release on April 13, 2026.** It will be included in the 78Vega branch opening on April 17, 2026. This guide is provided ahead of release for teams who want to prepare early.
    </Note>

    Anvil2 3.0 makes significant changes to how component styles are structured. If your application has custom CSS that targets or overrides Anvil2 component styles, this guide explains what changed, why, and what you may need to update.

    <Tip>
      Use the **`anvil2-migration` Claude skill** to get inline migration help as you work. It covers token renaming, specificity changes, color ramp remapping, and more. Find it in the [Anvil2 shared Claude skills](https://github.com/servicetitan/hammer/tree/main/shared-claude-skills).
    </Tip>

    ***

    ## Why this changed

    Before the 3.0 release, Anvil2 organized component styles inside CSS cascade layers (`@layer reset`, `@layer base`, `@layer state`). Cascade layers are a powerful tool for managing style precedence within a controlled environment — but they come with a critical constraint: **unlayered CSS always wins over layered CSS, regardless of source order or selector specificity.**

    The ServiceTitan monolith contains a large amount of CSS — including Bootstrap — that is not inside any layer, and realistically cannot be moved into one due to its age and the scope of that change. As a result, Bootstrap and other unlayered styles were overriding Anvil2's layered component styles in applications using the monolith.

    To work around this, Anvil2 shipped a `revert-layer` bugfix file that attempted to counteract the unlayered CSS. This file introduced its own problems — it was unreliable in MFEs where load order wasn't guaranteed — and teams continued to encounter broken styles.

    The root cause was a structural mismatch: Anvil2 was in layers, the rest of the CSS environment was not. The path we had to take was to remove layers from Anvil2 entirely.

    **In 3.0, all Anvil2 component styles are flat, unlayered CSS.** This puts Anvil2 on equal footing with Bootstrap and other global styles, eliminates the need for the `revert-layer` bugfix file, and makes the specificity model predictable and consistent across all environments.

    ***

    ## What changed

    ### CSS cascade layers removed

    All `@layer reset`, `@layer base`, and `@layer state` blocks have been removed from Anvil2 component styles. Component CSS is now standard flat CSS with no layer involvement.

    ### Component styles are scoped to `.anvil2`

    All Anvil2 component styles are automatically scoped under a `.anvil2` ancestor class at build time. This means every component style rule starts with `.anvil2` in the compiled output. This scoping gives Anvil2 styles one class's worth of extra specificity over unscoped global styles, such as Bootstrap, providing a natural boundary for overrides.

    The `.anvil2` class is applied by `AnvilProvider`. Any Anvil2 component rendered inside an `AnvilProvider` will be inside this scope.

    ### Published CSS Utils are scoped to `.anvil2`

    CSS utility classes are designed to be used **inside `AnvilProvider`**, which automatically adds the `anvil2` class to the page. Because of this, all `a2-*` rules in the published stylesheets are scoped — they only apply to elements inside an `anvil2` container.

    If you are currently using CSS utility classes outside of an `AnvilProvider`, either wrap it in an `AnvilProvider`, or add the `.anvil2` class name to an ancestor.

    ### The `revert-layer` bugfix file is removed

    Anvil2 previously shipped a CSS file that applied `revert-layer` fixes to counteract unlayered styles in the monolith overriding Anvil2's layered component styles. This file is no longer needed in 3.0 and has been removed from Anvil2.

    **If you are updating an MFE to Anvil2 3.0**, check whether your app is manually importing this file and remove it. Keeping it will have no positive effect and may cause unexpected style behavior.

    **For the monolith**, the file will be removed as part of a future platform update 78Vega.

    ### CSS custom properties and Tier 3 tokens

    Anvil2 3.0 introduces a new tier of design tokens — tier 3 (T3) component tokens — that sit between the semantic (tier 2) tokens and component styles. Rather than components referencing semantic tokens directly in their SCSS, each component now has its own dedicated token file (e.g., `button.tokens.json`, `checkbox.tokens.json`) that maps component-specific roles to semantic values. Component styles then reference these T3 variables via CSS custom properties (e.g., `--a2-mod-button-*`).

    This change does not affect consumers using Anvil2 components out of the box. However, if you were overriding component styles using internal CSS custom properties (e.g., `--a2-button-*`, `--a2-calendar-*`), those variables have been renamed to follow the `--a2-mod-{component}-*` convention. Refer to each component's updated SCSS for the new variable names.

    <Note>
      A full documentation page covering the Anvil2 design token system — including token tiers, naming conventions, and component token references — is coming soon.
    </Note>

    ### Color Ramps

    The 3.0 migration expands Color Ramps from 15 stops on the neutral ramp and 6 on non-neutral ramps to 20 stops on the neutral ramp and 12 on non-neutral ramps. Existing color tokens map to their previous counterparts. See [Figma](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=1-14817\&t=v7OG0ccy5riUwd2A-1) for details, including [ramp mappings](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=101-31661\&t=v7OG0ccy5riUwd2A-1) and [token mappings](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=103-2433\&t=v7OG0ccy5riUwd2A-1).

    ***

    ## Updating custom style overrides

    Any custom styles that target or override Anvil2 components must include `.anvil2` in their selector to achieve the necessary specificity. Because Anvil2 component styles are compiled with `.anvil2` as an ancestor, an override without it will not have enough specificity to win — even against the default Anvil2 styles, let alone Bootstrap.

    `.anvil2` can be applied in a number of ways depending on your situation:

    ### Unlayered CSS

    **Wrap an entire stylesheet** — useful when a whole file contains Anvil2 overrides:

    ```scss theme={null}
    .anvil2 {
      .my-button {
        background-color: blue;
      }

      .my-card {
        border-color: red;
      }
    }
    ```

    **Specificity to a single selector** — useful for a one-off override:

    ```scss theme={null}
    .anvil2 .my-component {
      background-color: blue;
    }
    ```

    <Note>
      **Using CSS Modules?** CSS Modules treat class names as locally scoped by default, so `.anvil2` in a module file refers to a hashed local class — not the global `.anvil2` scope Anvil2 needs. Wrap your selector in `:global()` to target the real `.anvil2` class:

      ```scss theme={null}
      :global(.anvil2) {
        .my-component {
          ...styles
        }
      }
      ```
    </Note>

    ### `@layer ____ {}`

    If your team wrapped Anvil2 overrides in `@layer` to take precedence over Anvil2's `@layer base` and `@layer state` blocks, that approach is no longer needed. Since layers no longer exist in 3.0, `@layer` has no effect on Anvil2 styles.

    Remove the `@layer` wrapper and replace it with `.anvil2`:

    ```css theme={null}
    /* 2.x */
    @layer application {
      .my-button {
        background-color: blue;
      }
      ... other Anvil2 style overrides ...
    }

    /* 3.0 */
    .anvil2 {
      .my-button {
        background-color: blue;
      }
      ... other Anvil2 style overrides ...
    }
    ```

    #### PostCSS plugin

    If your team has entire files dedicated to Anvil2 overrides, you can automate the `.anvil2` wrapping using a PostCSS plugin rather than wrapping each file manually. This is the same approach Anvil2 itself uses internally to scope its component styles. This can also be adapted for replacing `@layer ____ {}` with `.anvil2 { }`.

    Add the following plugin to your PostCSS config and apply it to the files that contain your Anvil2 overrides:

    ```js theme={null}
    // postcss.config.js

    const anvil2Wrapper = () => {
      return {
        postcssPlugin: "postcss-anvil2-wrapper",
        Once(root, { postcss, result }) {
          const inputFile = result.root.source?.input?.from;

          // Scope this plugin to only the files you want wrapped.
          // Update this condition to match your override file paths.
          if (!inputFile || !inputFile.includes("anvil2-overrides")) {
            return;
          }

          const nodes = [];
          const keyframeNodes = [];
          const globalNodes = [];

          root.each((node) => {
            // Keep @keyframes outside the wrapper
            if (node.type === "atrule" && node.name === "keyframes") {
              keyframeNodes.push(node.clone());
              node.remove();
            }
            // Keep :global selectors outside the wrapper
            else if (node.type === "rule" && node.selector.includes(":global")) {
              globalNodes.push(node.clone());
              node.remove();
            } else {
              nodes.push(node.clone());
              node.remove();
            }
          });

          if (nodes.length > 0) {
            const wrapper = new postcss.Rule({ selector: ".anvil2" });
            nodes.forEach((node) => wrapper.append(node));
            root.append(wrapper);
          }

          keyframeNodes.forEach((node) => root.append(node));
          globalNodes.forEach((node) => root.append(node));
        },
      };
    };
    anvil2Wrapper.postcss = true;

    module.exports = {
      plugins: [anvil2Wrapper()],
    };
    ```

    With this in place, a file like:

    ```scss theme={null}
    // anvil2-overrides.scss
    .my-button {
      background-color: blue;
    }
    ```

    will compile to:

    ```css theme={null}
    .anvil2 .my-button {
      background-color: blue;
    }
    ```

    ### CSS custom properties

    #### Internal property names changed

    Anvil2 component styles use internal CSS custom properties as part of their implementation. These are not part of the public API. If your codebase was setting properties like `--button-background-color` or similar bare names to override Anvil2 styles, those overrides will not work in 3.0 — the internal names have changed. Refer to the [Tier 3 tokens section above](#css-custom-properties-and-tier-3-tokens) for the new naming convention.

    ***

    ## Known Issues

    ### Anvil (A1) dialogs containing Anvil2 components

    If you have an Anvil (A1) dialog that renders Anvil2 components inside it, those components will lose their styles in 3.0. This is because dialogs portal their content out of the DOM — the rendered output is attached directly to the document body, outside the normal component tree. This means the `.anvil2` class provided by your app's `ThemeProvider` is no longer an ancestor, and Anvil2 component styles will not apply.

    This is only an issue when mixing A1 and A2. A2 dialogs handle this correctly on their own.

    ### Preferred: migrate to the Anvil2 Dialog

    The recommended fix is to replace the A1 dialog with the [Anvil2 Dialog](/docs/web/components/dialog/code). The Anvil2 Dialog renders its portal content inside the `.anvil2` scope automatically, so no additional setup is needed.

    When migrating to the Anvil2 Dialog, any A1 components inside the dialog that use popovers — such as A1 Select fields — must also be migrated to their Anvil2 equivalents (e.g., `SelectField`). The Anvil2 Dialog uses the browser's [top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer), which A1 components are not designed to work within. A1 popover-based components will render beneath the dialog rather than on top of it, making them unusable.

    #### Alternative: wrap with ThemeProvider

    If migrating to the A2 Dialog is not immediately feasible, wrap the Anvil2 content inside the A1 dialog with a `ThemeProvider`. This gives the portaled content its own `.anvil2` scope:

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

    function MyA1Dialog() {
      return (
        <LegacyDialog>
          <ThemeProvider>
            {/* Anvil2 components will now have the .anvil2 scope */}
            <Button>Save</Button>
            <TextField label="Name" />
          </ThemeProvider>
        </LegacyDialog>
      );
    }
    ```

    ***

    ### Known Chrome DevTools performance issue with CSS custom properties

    **This is a Chrome DevTools-only issue and does not affect your application's runtime performance.**

    There is a [known bug in Chrome](https://issues.chromium.org/issues/457696384) where CSS custom properties cause extremely expensive style recalculations inside DevTools. When an element's styles are resolved through a chain of cascaded custom properties, the DevTools style panel can take significantly longer to update when inspecting elements. This manifests as slow style panel updates and sluggish element inspection — it does not affect how your app behaves or performs for end users.

    This is relevant to Anvil2 3.0 because the refactor significantly increased the use of CSS custom properties across all components. If you notice the DevTools style panel feeling slow when inspecting Anvil2 components in Chrome after upgrading, this bug is the likely cause.

    <Note>
      This is a Chrome engine bug, not an Anvil2 bug, and it only affects the Chrome DevTools experience — not application performance. Firefox and Safari are not affected. Chrome's current plan is to ship a production fix in version 149, slated for June 2, 2026. We will update this page when the fix is released.
    </Note>

    ***

    ## Summary

    | Scenario                                      | What to do                                                                                       |
    | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
    | Custom styles not overriding Anvil2           | Add `.anvil2` to the selector or file wrapper                                                    |
    | Using CSS Modules                             | Use `:global(.anvil2)` as the wrapper to target the real `.anvil2` scope                         |
    | Using `@layer` for Anvil2 overrides           | Remove the layer wrapper; replace with `.anvil2 { }`                                             |
    | Overrides stopped working after upgrade       | Add `.anvil2` to match Anvil2's specificity scope                                                |
    | Large files of Anvil2 overrides               | Use the PostCSS plugin to automate `.anvil2` wrapping                                            |
    | Relying on internal CSS custom property names | Migrate to standard CSS overrides scoped to `.anvil2` or update to the new token/property system |
    | MFE importing the `revert-layer` bugfix file  | Remove the import — no longer needed in 3.0; monolith removal coming in 78Vega                   |
    | A1 dialog containing A2 components            | Migrate to A2 Dialog, or wrap A2 content with `ThemeProvider`                                    |
    | Chrome style performance regression           | No action needed — Chrome fix expected in v149 (June 2, 2026)                                    |
  </Tab>
</Tabs>
