Scope: next branch only
This does not affect any released line. It was introduced on next by d5fb1bf (refactor: migrate display composable to v0 createBreakpoints, #22710) and is present on origin/next today. dev and the 4.x line are unaffected.
Problem
After the migration, display.mobile is resolved through matchMedia, but useDisplay's mobileBreakpoint prop override still resolves through a raw display.width comparison. The two paths were both innerWidth-based before the commit and always agreed; now, at the same viewport, a component with an explicit mobileBreakpoint prop can report mobile while a component without one does not (or vice versa).
Evidence
packages/vuetify/src/composables/display.ts on origin/next.
display.mobile comes from v0 (:123-127, :140):
const breakpoint = createBreakpoints({
mobileBreakpoint: options?.mobileBreakpoint,
breakpoints: options?.thresholds,
ssr: isObject(ssr) ? ssr : ssr === true ? { clientWidth: 0 } : undefined,
})
...
mobile: breakpoint.isMobile,
and createBreakpoints' isMobile is matchMedia/index-based (packages/0/src/composables/useBreakpoints/index.ts:196):
isMobile.value = isNumber(mb)
? (SUPPORTS_MATCH_MEDIA ? !window.matchMedia(`(min-width: ${mb}px)`).matches : width.value < mb)
: index < names.indexOf(mobileBreakpoint as BreakpointName)
The prop override was not touched by d5fb1bf and is still width-based (display.ts:165-177):
const mobile = computed(() => {
if (props.mobile) {
return true
} else if (typeof props.mobileBreakpoint === 'number') {
return display.width.value < props.mobileBreakpoint // :169
} else if (props.mobileBreakpoint) {
return display.width.value < display.thresholds.value[props.mobileBreakpoint] // :171
} else if (props.mobile === null) {
return display.mobile.value // :173 — matchMedia path
} else {
return false
}
})
display.width is breakpoint.width, i.e. window.innerWidth. So :169 / :171 (innerWidth) and :173 (matchMedia) are two mechanisms behind one mobile computed.
Both prop branches diverge, not just the numeric one: for a named mobileBreakpoint, v0's isMobile compares matchMedia-derived band indices, while :171 compares raw width to the threshold.
Before the commit, display.ts at d5fb1bfee4^ resolved the instance value as const mobile = width.value < breakpointValue (:185) and the prop override as display.width.value < … (:240-243) — identical mechanism, guaranteed agreement.
Note: the pre-migration file is where :239-242 points. On origin/next the block has moved to :165-177.
Conditions
Any viewport where window.innerWidth and the CSS media-query width straddle the mobile threshold — fractional page zoom (the case that motivated the matchMedia switch, vuetifyjs/0#157), or a classic non-overlay scrollbar's ~15px. At such a viewport:
<v-card /> <!-- mobile ← matchMedia -->
<v-card mobile-breakpoint="md" /> <!-- mobile ← innerWidth -->
The two can disagree, producing inconsistent --mobile classes and layout branches within the same render.
Compounding factor: createDisplay calls the bare createBreakpoints() factory, and framework.ts:97 gates display.update() on options.ssr. In a non-SSR app the v0 initial state is itself seeded by width comparison and never corrected until the first resize — so the two paths agree at first paint and then diverge once a resize fires. Tracked separately as vuetifyjs/0#730.
Fix sketch
Resolve the prop override through the same mechanism as display.mobile. Options: build the numeric branch on matchMedia((min-width: ${n}px)), or have v0 expose a mobile predicate the prop path can call with an override threshold, so there is one implementation rather than two.
Context on milestone
The display migration was reassessed on 2026-07-27 and the decision was to keep it on next for v5, not to backport. Both consequences are already documented in the v5 upgrade guide by the migration commit itself:
- Returned Refs are now readonly.
- Breakpoints are matched with
window.matchMedia instead of window.innerWidth. This may result in slightly different values at zoom levels other than 100%.
So this is a live defect on an unreleased branch, to fix before v5 ships — not a regression on a released line.
Scope:
nextbranch onlyThis does not affect any released line. It was introduced on
nextby d5fb1bf (refactor: migrate display composable to v0 createBreakpoints, #22710) and is present onorigin/nexttoday.devand the 4.x line are unaffected.Problem
After the migration,
display.mobileis resolved throughmatchMedia, butuseDisplay'smobileBreakpointprop override still resolves through a rawdisplay.widthcomparison. The two paths were bothinnerWidth-based before the commit and always agreed; now, at the same viewport, a component with an explicitmobileBreakpointprop can report mobile while a component without one does not (or vice versa).Evidence
packages/vuetify/src/composables/display.tsonorigin/next.display.mobilecomes from v0 (:123-127,:140):and
createBreakpoints'isMobileis matchMedia/index-based (packages/0/src/composables/useBreakpoints/index.ts:196):The prop override was not touched by d5fb1bf and is still width-based (
display.ts:165-177):display.widthisbreakpoint.width, i.e.window.innerWidth. So:169/:171(innerWidth) and:173(matchMedia) are two mechanisms behind onemobilecomputed.Both prop branches diverge, not just the numeric one: for a named
mobileBreakpoint, v0'sisMobilecompares matchMedia-derived band indices, while:171compares raw width to the threshold.Before the commit,
display.tsatd5fb1bfee4^resolved the instance value asconst mobile = width.value < breakpointValue(:185) and the prop override asdisplay.width.value < …(:240-243) — identical mechanism, guaranteed agreement.Conditions
Any viewport where
window.innerWidthand the CSS media-query width straddle the mobile threshold — fractional page zoom (the case that motivated the matchMedia switch, vuetifyjs/0#157), or a classic non-overlay scrollbar's ~15px. At such a viewport:The two can disagree, producing inconsistent
--mobileclasses and layout branches within the same render.Compounding factor:
createDisplaycalls the barecreateBreakpoints()factory, andframework.ts:97gatesdisplay.update()onoptions.ssr. In a non-SSR app the v0 initial state is itself seeded by width comparison and never corrected until the first resize — so the two paths agree at first paint and then diverge once a resize fires. Tracked separately as vuetifyjs/0#730.Fix sketch
Resolve the prop override through the same mechanism as
display.mobile. Options: build the numeric branch onmatchMedia((min-width: ${n}px)), or have v0 expose a mobile predicate the prop path can call with an override threshold, so there is one implementation rather than two.Context on milestone
The display migration was reassessed on 2026-07-27 and the decision was to keep it on
nextfor v5, not to backport. Both consequences are already documented in the v5 upgrade guide by the migration commit itself:So this is a live defect on an unreleased branch, to fix before v5 ships — not a regression on a released line.