Skip to content

refactor(display): delegate breakpoint math to createBreakpoints - #23210

Open
johnleider wants to merge 5 commits into
devfrom
refactor/v0-display
Open

johnleider wants to merge 5 commits into
devfrom
refactor/v0-display

Conversation

@johnleider

@johnleider johnleider commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

createDisplay still returns the instance components already read: mobile, thresholds, platform, and the breakpoint flags. Flag calculation now comes from createBreakpoints.

Resize stays in Vuetify. The v0 plugin subscribes by replacing app.mount, and createVuetify already owns that hook for the SSR flush. A boolean ssr: true is passed as a zero size, because createBreakpoints only accepts a size object.

Checks use matchMedia when the browser has it, so zoom follows the CSS media query instead of innerWidth.

<template>
  <v-app theme="dark" v-resize="onResize">
    <v-container ref="container" class="mt-12 outline-dashed">
      <v-sheet class="mx-auto pa-6">
        <pre>{{ $vuetify.display.thresholds }}</pre>
        <v-table>
          <tbody>
            <tr>
              <td>Viewport width</td>
              <td class="text-right">
                <v-code class="text-body-large">{{ $vuetify.display.width }}px</v-code>
              </td>
            </tr>
            <tr>
              <td>Breakpoint</td>
              <td class="text-right">
                <v-code class="text-body-large">{{ $vuetify.display.name }}</v-code>
              </td>
            </tr>
            <tr>
              <td>VContainer width</td>
              <td class="text-right">
                <v-code class="text-body-large">{{ containerWidth }}px</v-code>
              </td>
            </tr>
            <tr>
              <td>innerWidth rounded up (seen at)</td>
              <td class="text-right">
                <v-code class="text-body-large">{{ roundedUp.join(', ') || 'never' }}</v-code>
              </td>
            </tr>
          </tbody>
        </v-table>
        <br>
        <v-code>cols: xs:12 / sm:6 / md:4 / lg:3 / xl:2 / xxl:1</v-code>
        <br>
        <v-row gap="4" wrap>
          <v-col
            v-for="i in 12"
            :key="i"
            cols="12"
            lg="3"
            md="4"
            sm="6"
            xl="2"
            xxl="1"
          >
            <div class="bg-red text-center py-2">{{ i }}</div>
          </v-col>
        </v-row>
        <br>
        <div :key="$vuetify.display.width" class="d-flex flex-wrap ga-2">
          <v-chip
            :color="$vuetify.display.mobile ? 'success' : 'error'"
            :prepend-icon="$vuetify.display.mobile ? 'mdi-check' : 'mdi-close'"
            text="mobile"
            label
          />
          <v-chip
            :color="local.mobile.value ? 'success' : 'error'"
            :prepend-icon="local.mobile.value ? 'mdi-check' : 'mdi-close'"
            :text="`mobile-breakpoint: ${$vuetify.display.mobileBreakpoint}`"
            label
          />
          <template v-for="key in flags" :key="key">
            <v-chip
              v-if="$vuetify.display[key]"
              :color="key.endsWith('AndUp') ? 'yellow' : key.endsWith('AndDown') ? '' : 'success'"
              :text="key"
              prepend-icon="mdi-check"
              label
            />
          </template>
        </div>
      </v-sheet>
    </v-container>
  </v-app>
</template>

<script setup lang="ts">
  import { type ComponentPublicInstance, ref, useTemplateRef } from 'vue'
  import { type DisplayInstance } from '@/types'
  import { useDisplay } from '@/composables/display'

  const { mobileBreakpoint } = useDisplay()
  const local = useDisplay({ mobile: null, mobileBreakpoint: mobileBreakpoint.value })

  const container = useTemplateRef<ComponentPublicInstance>('container')

  const containerWidth = ref(0)
  const roundedUp = ref<string[]>([])
  function onResize () {
    containerWidth.value = container.value?.$el.clientWidth
    if (!matchMedia(`(min-width: ${innerWidth}px)`).matches) {
      roundedUp.value = [...new Set([...roundedUp.value, `${innerWidth}px @ ${devicePixelRatio}`])]
    }
  }

  const flags = [
    'xs',
    ...'sm md lg xl xxl'.split(' ').flatMap(v => [`${v}AndDown`, v, `${v}AndUp`]),
  ] as (keyof DisplayInstance)[]
</script>

createDisplay still returns the instance components already read.
Flag calculation now comes from @vuetify/v0 createBreakpoints, which
follows matchMedia when the browser has it.

Resize stays here. The v0 plugin subscribes by replacing app.mount,
and createVuetify already owns that hook for the SSR flush.
@johnleider johnleider added this to the v4.3.0 milestone Sep 23, 2026
@johnleider johnleider added T: enhancement Functionality that enhances existing features framework Issues and Feature Requests that have needs framework-wide. labels Sep 23, 2026
@johnleider johnleider self-assigned this Sep 23, 2026
@vuetify/v0 1.2.3 listens inside createBreakpoints. A second listener
would run update twice per resize. The dependency floor is 1.2.3.
createBreakpoints attaches that listener on the first update(), which
runs after createVuetify's scope has returned. A child scope created
with the display is stopped with it.
@johnleider
johnleider requested a review from J-Sek September 24, 2026 20:45
@J-Sek

J-Sek commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

const mobile = computed(...) in line 212 is evaluated against width (effectively innerWidth) so can disagree with global mobile when mobileBreakpoint is set to lg. This is visible in the playground I pasted into the PR description.

In practice, devs would need to make use of mobile-breakpoint props setting them to the same value as global one which is pointless.. but still.. the inconsistency is a bit annoying.

The solution (evaluating mobile using useMediaQuery) bares some risk of performance degradation because listeners are not shared.

Comment on lines 215 to 218

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should make this part align with v0. I don't mind a split into separate PR though, as code duplication, reactivity and potential performance overhead are a concern (as hinted here)

The global mobile flag follows the CSS query. The component override
was still comparing innerWidth, so the same lg threshold could disagree.
The computed reads width, so it refreshes from the existing resize
listener and does not subscribe to matchMedia itself.
J-Sek
J-Sek previously approved these changes Sep 25, 2026
Comment thread packages/vuetify/src/composables/display.ts
Comment thread packages/vuetify/src/composables/display.ts
matchMedia() does not subscribe. Removing this read would freeze the
component mobile flag on resize.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework Issues and Feature Requests that have needs framework-wide. T: enhancement Functionality that enhances existing features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants