Skip to content

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

Open
AndreyYolkin wants to merge 1 commit into
mainfrom
fix/381-cascade-layer-order
Open

fix(styles): inline establishing cascade-layer order for Vuetify 4 (#381)#382
AndreyYolkin wants to merge 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
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

1 participant