CSS Cascade Layers

Understanding and working with Dialtone's CSS cascade layer architecture.

Dialtone uses CSS Cascade Layers (@layer) to organize all styles into a predictable hierarchy. This ensures utilities always override components, eliminates specificity wars, and makes it easy to integrate custom styles.

Understanding Dialtone's Layers

Dialtone defines four cascade layers in priority order:

@layer dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities;

Layer Priority

For normal (non-!important) declarations:

  1. dialtone.utilities - Highest priority (utility classes)
  2. dialtone.components - Component styles
  3. dialtone.base - Base styles (tokens, fonts, themes)
  4. dialtone.reset - Lowest priority (resets)

For !important declarations, the order reverses:

  1. dialtone.reset !important - Highest priority
  2. dialtone.base !important
  3. dialtone.components !important
  4. dialtone.utilities !important - Lowest priority

What's in Each Layer

  • dialtone.reset: CSS resets, e.g. (normalize.css), element resets (h1, h2)
  • dialtone.base: Design Tokens (CSS Variables), @font-face, themes, global html/body rules
  • dialtone.components: Component styles (buttons, inputs, modals), recipes, typography classes
  • dialtone.utilities: Utility classes with !important, generated utilities, responsive utilities

How Layers Affect the Cascade

Within the same layer, traditional CSS specificity applies:

@layer dialtone.utilities {
  .d-p16 { prop: value !important; }      /* specificity: 0-1-0 */
  div.d-p16 { prop: value !important; }   /* specificity: 0-1-1 - WINS */
}

Reference: Specificity Calculator

Across different layers, layer order wins (specificity is ignored):

@layer dialtone.components {
  div.d-card.d-card--active { prop: value; } /* specificity: 0-3-1 */
}

@layer dialtone.utilities {
  .d-fc-primary { prop: value !important; }   /* specificity: 0-1-0 - WINS */
}

For Dialtone Consumers

Writing Overrides

Option 1: Use Utility Classes (Recommended)

Dialtone utility classes are designed to override component styles:

<!-- Utility classes override component defaults -->
<button class="d-foo d-p-300 d-bgc-critical">
  Custom Button
</button>

Option 2: Application Layer

For app-specific styles, create your own layer after Dialtone's utilities:

/* In your app's CSS file */
@layer dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities, app;

@layer app {
  .my-custom-button {
    prop: value;
  }
}

Option 3: Unlayered Overrides (Use Sparingly)

Styles outside any @layer have the highest priority:

/* These override ALL layered styles (including Dialtone utilities) */
.my-critical-override {
  color: red !important;
}

Common Patterns

Override Dialtone Component Styles

Create an app layer after utilities:

@layer dialtone.reset, dialtone.base, dialtone.components, dialtone.utilities, app.overrides;

@layer app.overrides {
  .d-foo--custom {
    prop: value;
  }
}

Third-Party CSS Integration

If using third-party libraries, wrap them in a layer between components and utilities:

@layer dialtone.reset, dialtone.base, dialtone.components, third-party, dialtone.utilities;

@layer third-party {
  @import 'some-library/styles.css';
}

This ensures:

  • Third-party styles don't override Dialtone components unexpectedly
  • Dialtone utilities can still override third-party styles

What Happens Without Layers?

Styles not in a layer have higher priority than all layered styles for normal declarations:

/* Layered (lower priority) */
@layer dialtone.utilities {
  .d-fc-primary { color: blue !important; }
}

/* Unlayered (higher priority) */
.some-class { color: red; } /* WINS over layered utilities */

However, !important reverses this: layered !important beats unlayered !important.

Best Practices for Consumers

Do

  • Use Dialtone utility classes when possible
  • Create app layers after dialtone.utilities for custom styles
  • Wrap third-party CSS in layers
  • Keep unlayered styles to a minimum

Don’t

  • Add unlayered styles unless absolutely necessary
  • Create layers that conflict with Dialtone's layer names
  • Mix layered and unlayered styles in the same file without good reason

For Contributors

What Goes in Each Layer

dialtone.reset

  • CSS resets (normalize.css)
  • Typography resets (h1, h2, etc.)
  • Global element resets

Files: lib/build/less/dialtone-reset.less, typography reset section in utilities/typography.less

dialtone.base

  • Design tokens (CSS custom properties)
  • @font-face declarations
  • Theme styles
  • Global html/body rules
  • Vue transition classes

Files: Token CSS (via dialtone-tokens.cjs), dialtone-globals.less, dialtone-transitions.less, themes/default.less

dialtone.components

  • Component styles (buttons, inputs, modals, etc.)
  • Recipe styles (complex compositions)
  • Typography component classes, i.e. DtText
  • Layout compositions

Files: components/*.less (49 files), recipes/*.less (21 files), component sections in utility files

dialtone.utilities

  • Utility classes with !important
  • Generated utilities (colors, spacing, sizing, etc.)
  • Responsive utilities (.lg:d-d-block, etc.)
  • Hand-written utility classes

Files: utilities/*.less (10 files), generated by postcss/dialtone-generators.cjs and postcss-responsive-variations plugin

Adding New Styles

Adding a New Component

Wrap your component styles in @layer dialtone.components:

// lib/build/less/components/my-component.less
@layer dialtone.components {
  .d-my-component {
    property: value;

    &__header {
      property: value;
    }
  }
}

Import in dialtone.less:

@import 'components/my-component';

Adding New Utilities

Wrap utility classes in @layer dialtone.utilities:

// lib/build/less/utilities/my-utilities.less
@layer dialtone.utilities {
  .d-my-util { property: value !important; }
}

Parametric Mixins for Cross-Layer Access

If you need to share styles between files or layers, extract parametric mixins outside @layer blocks:

// Define OUTSIDE @layer for cross-file access
._my-mixin() {
  display: flex;
  align-items: center;
}

@layer dialtone.components {
  .d-component {
    ._my-mixin(); // ✅ Works
  }
}

Why: LESS treats @layer as a scope boundary. Mixins inside one layer cannot be accessed from another layer or file.

Don't Break the Layers

Don’t

Avoid adding styles outside @layer blocks:

// BAD - unlayered styles override everything
.d-my-class {
  property: value;
}

Don’t

Don't put utilities in the components layer:

@layer dialtone.components {
  // BAD - utilities belong in dialtone.utilities
  .d-mt16 { property: value !important; }
}

Don’t

Don't call mixins from inside different @layer blocks:

@layer dialtone.components {
  .d-foo() { /* ... */ }
}

@layer dialtone.utilities {
  .d-bar {
    .d-foo(); // ❌ BREAKS - mixin not accessible from `dialtone.components`
  }
}

Testing Your Changes

After adding or modifying layered styles:

  1. Build
  2. Verify layers: Check lib/dist/dialtone.css for proper @layer wrapping
  3. Lint: pnpm run lint - catches layer violations
  4. Test: Ensure utilities still override components in your browser

Resources

CSS Cascade Layers documentation last updated Thursday, June 18, 2026