vuetify-nuxt-module version
1.0.0-rc.1
Nuxt version
4.4.8
Vuetify version
4.1.2
Reproduction
see below
Steps to reproduce
Cascade-layer order is non-deterministic in dev (styles.configFile mode), letting the reset override component styles
Summary
In styles: { configFile } mode, the establishing @layer order declaration that
Vuetify ships in vuetify/lib/styles/generic/_layers.scss is not guaranteed to be
parsed before component styles during nuxt dev. Because @vuetify/unplugin-styles
injects each component's CSS as a separate Vite style module at runtime, the cascade-layer
priority ends up decided by style-injection order, which is non-deterministic.
When vuetify-components happens to register its layer name before vuetify-core, the
reset rule button, input, … { font: inherit } (in vuetify-core.reset) outranks
component rules such as .v-btn--size-small { font-size: var(--v-btn-size) } (in
vuetify-components). The visible result: a <v-btn size="small"> (rendered as a real
<button>) intermittently renders at 16px (inherited body size) instead of 12px.
It is intermittent — different page loads of the same dev server, with no code change,
flip between 12px and 16px.
Environment
|
|
vuetify-nuxt-module |
1.0.0-rc.1 |
vuetify |
4.1.2 |
@vuetify/unplugin-styles |
1.0.0-beta.11 |
| Nuxt |
4.4.8 (Nitro 2.13.4, Vite 7.3.6, Vue 3.5.39) |
| Node |
24.18.0 |
| OS / Browser |
macOS 14 (darwin 24.6.0) / Chrome (any layer-aware browser) |
Relevant config (nuxt.config.ts):
export default defineNuxtConfig({
modules: ['vuetify-nuxt-module'],
vuetify: {
moduleOptions: {
styles: { configFile: './assets/css/settings.scss' },
},
},
})
assets/css/settings.scss:
@use 'vuetify/settings' with (
$body-font-family: ('DM Sans', sans-serif),
);
Steps to reproduce
- Set up a Nuxt 4 app with
vuetify-nuxt-module using styles.configFile (above).
- Put a real button (no
to/href, so it renders as <button>, which the reset
targets) somewhere visible:
<v-btn size="small">Small</v-btn>
npm run dev, open the page, inspect the button's computed font-size.
- Hard-refresh / open the page in several fresh tabs.
Expected: font-size: 12px every time (.v-btn--size-small → --v-btn-size: .75rem).
Actual: font-size is intermittently 16px. DevTools shows the reset winning:
button, input, optgroup, select, textarea { font: inherit; } @layer vuetify-core.reset ← applied
.v-btn--size-small { font-size: var(--v-btn-size); } @layer vuetify-components ← struck through
Because the symptom depends on injection order, a manual repro is flaky. The script below
makes it deterministic and observable.
Deterministic reproduction script
Drives real Chrome against the running dev server (http://localhost:3000), injects a
<button class="v-btn v-btn--size-small">, and reports the computed font-size plus the
first-appearance index of each layer's block. Run several times — firstComp < firstCore
correlates with the 16px result.
// repro.mjs — node repro.mjs (npm i puppeteer-core; uses system Chrome)
import puppeteer from 'puppeteer-core';
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const BASE = 'http://localhost:3000';
const PAGES = ['/'];
const browser = await puppeteer.launch({ executablePath: CHROME, headless: 'shell' });
for (let i = 1; i <= 6; i++) {
for (const path of PAGES) {
const page = await browser.newPage();
await page.setCacheEnabled(false);
await page.goto(BASE + path, { waitUntil: 'domcontentloaded' });
await new Promise(r => setTimeout(r, 1500));
const r = await page.evaluate(() => {
const b = document.createElement('button');
b.className = 'v-btn v-btn--size-small';
b.textContent = 'X';
document.body.appendChild(b);
let firstCore = Infinity, firstComp = Infinity, idx = 0;
const walk = (rules) => { for (const rule of rules) { idx++;
if (rule.constructor.name === 'CSSLayerBlockRule') {
if (rule.name === 'vuetify-core' && firstCore === Infinity) firstCore = idx;
if (rule.name === 'vuetify-components' && firstComp === Infinity) firstComp = idx;
}
if (rule.cssRules) walk(rule.cssRules);
} };
for (const s of document.styleSheets) { try { walk(s.cssRules); } catch {} }
return { fontSize: getComputedStyle(b).fontSize, firstCore, firstComp };
});
console.log(`[${r.fontSize === '12px' ? 'ok ' : 'BUG'}] ${path} font-size=${r.fontSize} firstCore=${r.firstCore} firstComp=${r.firstComp}`);
await page.close();
}
}
await browser.close();
Observed output (same dev server, no code change)
[BUG] / font-size=16px firstCore=6678 firstComp=5984 ← components registered before core
[BUG] / font-size=16px firstCore=6644 firstComp=5984
[ok ] / font-size=12px firstCore=5992 firstComp=6153 ← core registered before components
...
The browser never sees an establishing @layer vuetify-core, vuetify-components, …;
statement in this mode — only block rules like @layer vuetify-components { … } — so the
order is whatever the injection sequence happens to be.
Root cause
- In
configFile mode the module relies on @vuetify/unplugin-styles to inject component
CSS on demand. In dev these are individual Vite style modules added to <head> at
runtime.
- The establishing order block from
_layers.scss
(@layer vuetify-core { … } @layer vuetify-components; …) is effectively dropped /
tree-shaken, so it does not pin the order ahead of component styles.
- Per CSS Cascade 5, layer priority is set by the order layer names are first declared.
With no establishing statement parsed first, first-declaration = first-injection, which
Vite does not guarantee. When vuetify-components lands first it becomes the lower
priority layer, and vuetify-core.reset (declared later) wins — so font: inherit
overrides .v-btn--size-*.
Workaround (confirmed)
Emit the establishing layer order as an inline <style> in the SSR'd <head>, so it's
parsed during initial HTML parse, before any runtime-injected component style:
// nuxt.config.ts
app: {
head: {
style: [{
innerHTML: '@layer vuetify-core,vuetify-components,vuetify-overrides,vuetify-utilities,vuetify-final;',
tagPriority: -100,
}],
},
},
With this, the script reports 12px on 12/12 loads, even when firstComp < firstCore
(the inline statement has already locked the order before the blocks are seen).
Note: a css: ['~/layers.css'] file containing the same @layer statement does not
work in dev — it's just another runtime-injected style module and loses the same
injection-order race (~50% of loads still showed 16px). It has to be an inline head style.
Suggested fix
In configFile (and treeshaking) mode, have the module guarantee the establishing
@layer vuetify-core, vuetify-components, vuetify-overrides, vuetify-utilities, vuetify-final;
declaration is parsed before any component style — e.g. inject it as an inline head style
during SSR, or as the first entry of the styles graph in dev — so cascade-layer priority
doesn't depend on Vite's injection order.
Expected behavior
Buttons should always be the correct size
Actual behavior
Buttons are the incorrect size
System info
Additional context
No response
Validations
vuetify-nuxt-module version
1.0.0-rc.1
Nuxt version
4.4.8
Vuetify version
4.1.2
Reproduction
see below
Steps to reproduce
Cascade-layer order is non-deterministic in dev (
styles.configFilemode), letting the reset override component stylesSummary
In
styles: { configFile }mode, the establishing@layerorder declaration thatVuetify ships in
vuetify/lib/styles/generic/_layers.scssis not guaranteed to beparsed before component styles during
nuxt dev. Because@vuetify/unplugin-stylesinjects each component's CSS as a separate Vite style module at runtime, the cascade-layer
priority ends up decided by style-injection order, which is non-deterministic.
When
vuetify-componentshappens to register its layer name beforevuetify-core, thereset rule
button, input, … { font: inherit }(invuetify-core.reset) outrankscomponent rules such as
.v-btn--size-small { font-size: var(--v-btn-size) }(invuetify-components). The visible result: a<v-btn size="small">(rendered as a real<button>) intermittently renders at 16px (inherited body size) instead of 12px.It is intermittent — different page loads of the same dev server, with no code change,
flip between 12px and 16px.
Environment
vuetify-nuxt-module1.0.0-rc.1vuetify4.1.2@vuetify/unplugin-styles1.0.0-beta.114.4.8(Nitro2.13.4, Vite7.3.6, Vue3.5.39)24.18.0Relevant config (
nuxt.config.ts):assets/css/settings.scss:Steps to reproduce
vuetify-nuxt-moduleusingstyles.configFile(above).to/href, so it renders as<button>, which the resettargets) somewhere visible:
npm run dev, open the page, inspect the button's computedfont-size.Expected:
font-size: 12pxevery time (.v-btn--size-small→--v-btn-size: .75rem).Actual:
font-sizeis intermittently16px. DevTools shows the reset winning:Because the symptom depends on injection order, a manual repro is flaky. The script below
makes it deterministic and observable.
Deterministic reproduction script
Drives real Chrome against the running dev server (
http://localhost:3000), injects a<button class="v-btn v-btn--size-small">, and reports the computedfont-sizeplus thefirst-appearance index of each layer's block. Run several times —
firstComp < firstCorecorrelates with the 16px result.
Observed output (same dev server, no code change)
The browser never sees an establishing
@layer vuetify-core, vuetify-components, …;statement in this mode — only block rules like
@layer vuetify-components { … }— so theorder is whatever the injection sequence happens to be.
Root cause
configFilemode the module relies on@vuetify/unplugin-stylesto inject componentCSS on demand. In dev these are individual Vite style modules added to
<head>atruntime.
_layers.scss(
@layer vuetify-core { … } @layer vuetify-components; …) is effectively dropped /tree-shaken, so it does not pin the order ahead of component styles.
With no establishing statement parsed first, first-declaration = first-injection, which
Vite does not guarantee. When
vuetify-componentslands first it becomes the lowerpriority layer, and
vuetify-core.reset(declared later) wins — sofont: inheritoverrides
.v-btn--size-*.Workaround (confirmed)
Emit the establishing layer order as an inline
<style>in the SSR'd<head>, so it'sparsed during initial HTML parse, before any runtime-injected component style:
With this, the script reports 12px on 12/12 loads, even when
firstComp < firstCore(the inline statement has already locked the order before the blocks are seen).
Suggested fix
In
configFile(and treeshaking) mode, have the module guarantee the establishing@layer vuetify-core, vuetify-components, vuetify-overrides, vuetify-utilities, vuetify-final;declaration is parsed before any component style — e.g. inject it as an inline head style
during SSR, or as the first entry of the styles graph in dev — so cascade-layer priority
doesn't depend on Vite's injection order.
Expected behavior
Buttons should always be the correct size
Actual behavior
Buttons are the incorrect size
System info
Additional context
No response
Validations