Skip to content

fix(display): use matchMedia for mobile breakpoint in useDisplay composable at display.ts (Fixes #23042) - #23129

Open
Tauromachian wants to merge 2 commits into
vuetifyjs:nextfrom
Tauromachian:next
Open

fix(display): use matchMedia for mobile breakpoint in useDisplay composable at display.ts (Fixes #23042)#23129
Tauromachian wants to merge 2 commits into
vuetifyjs:nextfrom
Tauromachian:next

Conversation

@Tauromachian

@Tauromachian Tauromachian commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Description

This is needed because createBreakpoint and useDisplay use different methods to determine the mobile computed breakpoint.

I Updated useDisplay composable inside of packages/vuetify/src/composables/display.ts on origin/next to use matchMedia in a similar fashion to createBreakpoints.

Fixes #23042

Markup:

There are a lot of cases I wanted to cover, so it makes for a large Playground.vue file.

<script setup>
  import { defineComponent } from '@/util'
  import { computed, h, ref } from 'vue'
  import { useDisplay } from 'vuetify'

  const display = useDisplay()

  const width = computed(() => display.width.value)
  const name = computed(() => display.name.value)
  const globalMobile = computed(() => display.mobile.value)

  const bpNumber = ref(800)
  const bpName = ref('md')
  const mobileOverride = ref(null)
  const drawerOpen = ref(true)

  const MobileProbe = defineComponent({
    name: 'MobileProbe',
    props: {
      name: { type: String, required: true },
      mobile: { type: null, default: null },
      mobileBreakpoint: { type: [Number, String], default: undefined },
    },
    setup (props) {
      const { mobile, displayClasses } = useDisplay(props)

      return () => h('div', { class: ['probe', displayClasses] }, [
        h('strong', `${props.name}: `),
        h('span', { class: 'ml-2' }, `mobile=${mobile.value}`),
        h(
          'div',
          { class: 'text-caption mt-1 text-grey-darken-1' },
          `classes: ${JSON.stringify(displayClasses.value)}`,
        ),
      ])
    },
  })
</script>

<template>
  <v-app>
    <v-container :class="['pa-6', drawerOpen && 'shifted']">
      <h1 class="text-h5 mb-4">useDisplay harness (matchMedia)</h1>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">Live readouts</div>
        <div>
          Viewport width: <strong>{{ width }}px</strong> ·
          name: <strong>{{ name }}</strong> ·
          display.mobile: <strong>{{ globalMobile }}</strong>
        </div>
        <div class="text-caption mt-2 text-grey-darken-1">
          Resize via DevTools device toolbar (Cmd/Ctrl+Shift+M) to switch breakpoints.
          Thresholds (defaults): xs=0, sm=600, md=840, lg=1145, xl=1545, xxl=2138.
        </div>
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          A · No mobileBreakpoint (fall-through to display.mobile)
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          Neither <code>mobile</code> nor <code>mobileBreakpoint</code> is set, so
          <code>mobile === null</code> → falls through to
          <code>display.mobile.value</code>.
        </div>
        <MobileProbe :mobile="null" name="A" />
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          B · Numeric mobileBreakpoint
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          Numeric breakpoints are passed verbatim to matchMedia.
          Drag the slider to verify the listener re-binds.
        </div>
        <v-slider
          v-model="bpNumber"
          :max="2000"
          :min="0"
          :step="10"
          class="mb-2"
          thumb-label
        />
        <MobileProbe :mobile-breakpoint="bpNumber" name="B" />
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          C · Named mobileBreakpoint
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          Named breakpoints are looked up in <code>display.thresholds</code>.
          Try each to verify the threshold resolution.
        </div>
        <v-select
          v-model="bpName"
          :items="['xs', 'sm', 'md', 'lg', 'xl', 'xxl']"
          class="mb-2"
        />
        <MobileProbe :mobile-breakpoint="bpName" name="C" />
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          D · mobile prop precedence
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          Explicit <code>mobile</code> truthy wins over breakpoint.
          <code>false</code> forces desktop.
          <code>null</code> falls through.
        </div>
        <v-btn-toggle
          v-model="mobileOverride"
          class="mb-2"
          divided
          mandatory
        >
          <v-btn :value="null">null (auto)</v-btn>
          <v-btn :value="false">false</v-btn>
          <v-btn :value="true">true</v-btn>
        </v-btn-toggle>
        <MobileProbe
          :mobile="mobileOverride"
          :mobile-breakpoint="800"
          name="D"
        />
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          E · Real consumers (VNavigationDrawer)
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          Components that use <code>makeDisplayProps</code> internally.
          Resize to verify layout flips are now matchMedia-driven.
        </div>
        <v-switch
          v-model="drawerOpen"
          class="mb-2"
          label="Open drawer"
        />
        <v-navigation-drawer
          v-model="drawerOpen"
          :class="['bg-grey-lighten-3', 'mb-3', drawerOpen && 'mt-3']"
          :mobile-breakpoint="lg"
        >
          <v-list density="compact" nav>
            <v-list-item title="Drawer item 1" />
            <v-list-item title="Drawer item 2" />
            <v-list-item title="Drawer item 3" />
          </v-list>
        </v-navigation-drawer>
      </v-card>

      <v-card class="mb-4 pa-4" variant="outlined">
        <div class="text-subtitle-2 mb-2">
          F · displayClasses (v-{`{name}`}--mobile)
        </div>
        <div class="text-caption text-grey-darken-1 mb-2">
          The component name passed as second arg controls the
          <code>v-{`{name}`}--mobile</code> class surface. Green = desktop,
          red = mobile.
        </div>
        <div class="bg-grey-lighten-4 pa-4 rounded">
          <MobileProbe :mobile-breakpoint="bpNumber" name="F" />
          <MobileProbe :mobile-breakpoint="bpName" name="F" />
        </div>
      </v-card>
    </v-container>
  </v-app>
</template>

<style scoped>
.shifted {
  padding-left: 260px;
}
.probe {
  padding: 8px 12px;
  border-radius: 4px;
  font-family: monospace;
  background: #fff;
  display: inline-block;
  margin: 4px;
}
.probe[class*='--mobile'] {
  outline: 2px solid #d32f2f;
  background: #ffebee;
}
.probe:not([class*='--mobile']) {
  outline: 2px solid #2e7d32;
  background: #e8f5e9;
}
</style>

@Tauromachian Tauromachian changed the title Next Use matchMedia for mobile breakpoint in useDisplay composable at display.ts Aug 17, 2026
@Tauromachian Tauromachian changed the title Use matchMedia for mobile breakpoint in useDisplay composable at display.ts fix(display): use matchMedia for mobile breakpoint in useDisplay composable at display.ts Aug 17, 2026
@Tauromachian Tauromachian changed the title fix(display): use matchMedia for mobile breakpoint in useDisplay composable at display.ts fix(display): use matchMedia for mobile breakpoint in useDisplay composable at display.ts (Fixes #23042) Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant