Skip to content

[Bug Report] display.mobile disagrees with the mobileBreakpoint prop path (next only) #23042

Description

@johnleider

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.

Metadata

Metadata

Assignees

Labels

T: bugFunctionality that does not work as intended/expected

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions