Skip to content

fix(styles): inline establishing cascade-layer order for Vuetify 4 (#381) - #382

Merged
johnleider merged 1 commit into
mainfrom
fix/381-cascade-layer-order
Sep 15, 2026
Merged

johnleider merged 1 commit into
mainfrom
fix/381-cascade-layer-order

Conversation

@AndreyYolkin

Copy link
Copy Markdown
Contributor

Summary

Fixes #381 — on Vuetify 4, <v-btn size="small"> (and other size/density tokens) intermittently render at the wrong size because the CSS cascade-layer order is non-deterministic.

Root cause

In styles treeshaking modes, Vuetify 4 component styles are injected on demand by @vuetify/unplugin-styles, each carrying its own @layer vuetify-components { … } block. Per CSS Cascade 5, layer priority is fixed by the order layer names are first declared. With no establishing statement guaranteed to be parsed first, first-declaration = injection order, which is:

  • non-deterministic in nuxt dev (Vite injects each style module at runtime), and
  • chunk-order-dependent in production (visible on hard refresh; SPA nav is fine).

When vuetify-components is declared before vuetify-core, vuetify-core.reset (button { font: inherit }) outranks .v-btn--size-* → the button renders at the inherited body size.

Vuetify ships the order in generic/_layers.scss, but it's just one more racer — and nuxt.options.css ordering can't fix it because the racing component styles are injected outside the css graph entirely (the reporter confirmed a css: ['~/layers.css'] file still raced).

The fix

Inline the establishing @layer statement into the SSR'd <head> (tagPriority: -100) so it's parsed before any runtime-injected or critical-inlined component style. One mechanism covers dev, prod, SSR and SPA.

@layer vuetify-core,vuetify-components,vuetify-overrides,vuetify-utilities,vuetify-final;

Scope / guards

  • Vuetify 4 only — v3's layers are opt-in (since 3.8), differently named, and under a single top-level vuetify layer (no top-level race).
  • Skipped for styles: 'none'.

New option: cascadeLayers

A flat @layer statement freezes the named layers contiguously, so a later-declared layer can only append. To keep custom interleaving possible, the injected order is configurable:

vuetify: {
  moduleOptions: {
    // omit            → Vuetify's default order (the fix, zero config)
    cascadeLayers: [   // custom order — slot your own layer between Vuetify's
      'vuetify-core', 'vuetify-components',
      'my-overrides', 'vuetify-overrides',
      'vuetify-utilities', 'vuetify-final',
    ],
    // cascadeLayers: false  // inject nothing; manage the order yourself
  }
}

VuetifyCascadeLayer offers the known Vuetify layer names as autocomplete while still allowing arbitrary custom names.

Upgrade note for existing workarounds

  • A default-order workaround (e.g. the common inline-head-style snippet) can simply be removed — the module now injects the same statement; leaving it is a harmless no-op.
  • A custom-interleaved order delivered via a css:/SCSS file should move to cascadeLayers (or set cascadeLayers: false), otherwise the default statement parsed first would push the custom layer to the end. Documented in the styling guide.

Tests

  • test/cascade-layers-head.test.ts — 13 pure-helper cases (default / custom array / empty / false / 'none' / v3 / core-before-components).
  • test/sass.test.ts — v4 configFile fixture: the SSR head contains the statement.
  • test/basic-vuetify3.test.ts — v3 fixture: the SSR head does not contain it.

Full suite 72 passing; lint clean on changed source; prepack build green with the type shipped into dist/module.d.mts.

Closes #381

)

In styles treeshaking modes, Vuetify 4 component styles are injected on
demand, each carrying its own `@layer vuetify-components { … }` block. With
no establishing statement parsed first, layer priority is decided by
injection order (non-deterministic in dev, chunk order in prod), so
`vuetify-core.reset` can outrank component rules — e.g. a real `<button>`
`<v-btn size="small">` renders at the inherited font size, not its token.

Inline the establishing `@layer` order into the SSR'd <head> (tagPriority
-100), so it is parsed before any runtime-injected or critical-inlined
component style. Vuetify 4 only (v3 layers are opt-in and differently
named); skipped for `styles: 'none'`.

Add a `cascadeLayers` option to customise the order (e.g. to interleave a
custom layer between Vuetify's) or disable injection with `false`.

Closes #381
@AndreyYolkin AndreyYolkin added area: styles Styles, SCSS, FOUC, style config priority: p2: high High: common bug, strong friction labels Jun 28, 2026
@AndreyYolkin AndreyYolkin added this to the 1.0.0 milestone Jun 28, 2026
@johnleider
johnleider merged commit ac7361a into main Sep 15, 2026
4 checks passed
@johnleider
johnleider deleted the fix/381-cascade-layer-order branch September 15, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: styles Styles, SCSS, FOUC, style config priority: p2: high High: common bug, strong friction

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cascade-layer order is non-deterministic in dev (styles.configFile mode), letting the reset override component styles

2 participants