Skip to main content

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.

Migrating from Anvil2 2.x to 3.0

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

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 to the element rendered by ThemeProvider. Any Anvil2 component rendered inside a ThemeProvider will be inside this scope.

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.
A full documentation page covering the Anvil2 design token system — including token tiers, naming conventions, and component token references — is coming soon.

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 for details, including ramp mappings and token mappings.

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:
.anvil2 {
  .my-button {
    background-color: blue;
  }

  .my-card {
    border-color: red;
  }
}
Specificity to a single selector — useful for a one-off override:
.anvil2 .my-component {
  background-color: blue;
}

@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:
/* 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:
// 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:
// anvil2-overrides.scss
.my-button {
  background-color: blue;
}
will compile to:
.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 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. 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, 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:
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 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.
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.

Summary

ScenarioWhat to do
Custom styles not overriding Anvil2Add .anvil2 to the selector or file wrapper
Using @layer for Anvil2 overridesRemove the layer wrapper; replace with .anvil2 { }
Overrides stopped working after upgradeAdd .anvil2 to match Anvil2’s specificity scope
Large files of Anvil2 overridesUse the PostCSS plugin to automate .anvil2 wrapping
Relying on internal CSS custom property namesMigrate to standard CSS overrides scoped to .anvil2 or update to the new token/property system
MFE importing the revert-layer bugfix fileRemove the import — no longer needed in 3.0; monolith removal coming in 78Vega
A1 dialog containing A2 componentsMigrate to A2 Dialog, or wrap A2 content with ThemeProvider
Chrome style performance regressionNo action needed — Chrome fix expected in v149 (June 2, 2026)
Last modified on March 18, 2026