Dialtone now uses CSS Cascade Layers to organize all styles into a predictable hierarchy, improving specificity control and making overrides more predictable.
Dialtone now uses CSS Cascade Layers (@layer) to organize all styles into a predictable hierarchy. This improves specificity control, makes overrides more predictable, and eliminates the need for complex selector specificity hacks.
For Consumers: This is a breaking change. Any app CSS that isn't wrapped in a named @layer will now unconditionally win over all Dialtone styles, regardless of specificity. Apps upgrading from Dialtone ≤9 will likely need to either adopt layers or use the no-layers build.
For Contributors: All new styles must be wrapped in the appropriate @layer block. See the CSS Cascade Layers Guide for details.
All Dialtone CSS is now organized into four cascade layers:
@layer dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities;
dialtone.reset - CSS resets (normalize.css, typography resets)dialtone.base - Design tokens, fonts, themes, global stylesdialtone.components - Component styles (buttons, inputs, modals, etc.)dialtone.utilities - Utility classes (spacing, colors, layout)Previously, ensuring utilities could override components required:
!important to every utilityWith cascade layers:
dialtone.utilitiesdialtone.componentsdialtone.basedialtone.reset (lowest)!important (Order Reverses!) #dialtone.reset !important (highest)dialtone.base !importantdialtone.components !importantdialtone.utilities !important!important (lowest)Why utilities use
!important: Within the layered system, Dialtone utility!importantdeclarations outrank all other layered!importantdeclarations and — because of how layers reverse!importantpriority — also outrank your unlayered!important. This guarantees utilities always win over components, but means consumer!importantoverrides targeting the same properties will not work. Put overrides in a named layer declared afterdialtone.utilitiesinstead.
Because all Dialtone styles are now inside named @layer blocks, any unlayered CSS in your app automatically wins over Dialtone — regardless of specificity or source order. This is how the cascade layer spec works, and it has two practical consequences:
1. Your app styles may unexpectedly override Dialtone
If your app has unlayered CSS targeting the same elements as Dialtone components, those rules now win even if they have lower specificity. You may see Dialtone component styles disappearing or being partially overridden where they previously showed correctly.
2. Your !important overrides now lose to Dialtone's layered !important
With layers, !important priority order reverses: Dialtone's !important utility classes are inside @layer dialtone.utilities, so your unlayered !important has lower priority than Dialtone's. Overrides that previously used !important to beat Dialtone may stop working.
Think about the intent of each piece of your app CSS before deciding where it belongs in the layer order:
@layer app.base, dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities, app.overrides;
@layer app.base {
/* Foundational styles Dialtone can override */
}
@layer app.overrides {
/* Component and page styles that override Dialtone */
}
Specificity works as expected within each layer, so you don't need to increase selector specificity to win — layer order handles it.
If you cannot migrate your app CSS to use layers, use the no-layers build instead.
If your project cannot use CSS Cascade Layers — older browser requirements, an existing specificity system, or a bundler that doesn't support @layer — import the no-layers variant instead:
// @dialpad/dialtone-css
import '@dialpad/dialtone-css/no-layers';
import '@dialpad/dialtone-css/no-layers/default-theme'; // optional theme
// @dialpad/dialtone
import '@dialpad/dialtone/css/no-layers';
import '@dialpad/dialtone/css/no-layers/default-theme'; // optional theme
This build is identical in output — all the same classes — but no @layer wrappers are present.
If you import token CSS directly from @dialpad/dialtone-tokens (rather than through @dialpad/dialtone-css or @dialpad/dialtone), use its own no-layers variant instead:
import '@dialpad/dialtone-tokens/no-layers/tokens-base-light.css';
import '@dialpad/dialtone-tokens/no-layers/tokens-dp-light.css';
If you theme at runtime with initDialtoneTheme from @dialpad/dialtone-tokens/themes/config, pass { layers: false } to load the no-layers core tokens instead:
import { initDialtoneTheme } from '@dialpad/dialtone/themes/config';
import Dp from '@dialpad/dialtone/themes/dp';
initDialtoneTheme(Dp, 'light', document.documentElement, { layers: false });
setBrand, setContrast, and setMaterial need no equivalent option — brand, contrast, and material overrides were never wrapped in @layer to begin with.
If you're using third-party CSS that conflicts with Dialtone, wrap it in a layer:
@layer dialtone.reset, dialtone.base, dialtone.components, third-party, dialtone.utilities;
@layer third-party {
@import 'some-library/styles.css';
}
This ensures Dialtone utilities can still override third-party styles.
When adding new styles, wrap them in the appropriate layer:
Components:
@layer dialtone.components {
.d-my-component {
/* styles */
}
}
Utilities:
@layer dialtone.utilities {
.d-my-util { property: value !important; }
}
To share styles between layers, extract parametric mixins outside @layer blocks:
// Outside @layer for cross-file access
._my-mixin() {
display: flex;
align-items: center;
}
@layer dialtone.components {
.d-component { ._my-mixin(); }
}
The build pipeline now validates that all Dialtone classes are properly layered. Unlayered classes will fail CI.
See the CSS Layers Contributor Guide for complete documentation.
<!-- Component default: blue background -->
<button class="d-btn d-bgc-critical">
<!-- Utility wins: red background -->
</button>
Responsive utilities are now in the same layer as base utilities, ensuring consistent behavior:
<div class="d-d-none lg:d-d-block">
<!-- Hidden by default, visible on large screens -->
</div>
@layer app.overrides {
.d-btn--custom {
border-radius: 999px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
}
CSS Cascade Layers are supported in all modern browsers:
If you need to support older browsers, use the no-layers build instead of relying on undefined fallback behavior.
Declare named app layers and place your CSS based on intent — foundational styles before Dialtone, overrides after. This is the forward-compatible path and gives you the full benefits of the cascade layer system.
@layer app.base, dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities, app.overrides;
@layer app.base {
/* Foundational styles: resets, base element defaults, your own tokens */
}
@layer app.overrides {
/* Overrides: component customizations, feature-specific layout rules */
}
See Using CSS Layers with Dialtone for detailed guidance on layer ordering, third-party CSS, and per-component overrides.
If adopting layers is not feasible, switch to the no-layers CSS import. All Dialtone classes are present; @layer wrappers are stripped at build time. Specificity and cascade behave as they did in Dialtone ≤9.
import '@dialpad/dialtone/css/no-layers'; // @dialpad/dialtone
import '@dialpad/dialtone-css/no-layers'; // @dialpad/dialtone-css
CSS Cascade Layers documentation last updated Friday, September 4, 2026
fix/popover-modal-zindex-scope