ui icon indicating copy to clipboard operation
ui copied to clipboard

fix(SelectMenu): correct virtualization height estimation when using item-description slot

Open aligu7 opened this issue 3 months ago โ€ข 2 comments

๐Ÿ”— Linked issue

This PR fixes a virtualization bug when using item-description slot with SelectMenu. No existing issue was filed as the bug was discovered and fixed directly.

โ“ Type of change

  • [ ] ๐Ÿ“– Documentation (updates to the documentation or readme)
  • [x] ๐Ÿž Bug fix (a non-breaking change that fixes an issue)
  • [ ] ๐Ÿ‘Œ Enhancement (improving an existing functionality)
  • [ ] โœจ New feature (a non-breaking change that adds functionality)
  • [ ] ๐Ÿงน Chore (updates to the build process or auxiliary tools and libraries)
  • [ ] โš ๏ธ Breaking change (fix or feature that would cause existing functionality to change)

๐Ÿ“š Description

Problem: When using the item-description slot with virtualize enabled in SelectMenu, items overlap and scrolling breaks due to incorrect height estimation.

Visual Evidence:

Before (Broken): image Items overlap because virtualizer estimates 32px height but actual rendered height is 52px with descriptions

After (Fixed): image Items render correctly with proper 52px spacing when descriptions are present

Reproduction:

<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items: SelectMenuItem[] = Array(1000)
  .fill(0)
  .map((_, i) => ({
    label: `item-${i}`,
    value: i
  }))
</script>

<template>
  <USelectMenu
    :items="items"
    virtualize
    class="w-48"
  >
    <template #item-description="{ item }">
      Custom description for {{ item.label }}
    </template>
  </USelectMenu>
</template>

Root Cause:

The original virtualizerProps used a fixed estimateSize of 24-40px (based on size) for all items, regardless of whether they had descriptions:

// Before- always used small sizes
const virtualizerProps = toRef(() => !!props.virtualize && defu(typeof props.virtualize === 'boolean' ? {} : props.virtualize, {
  estimateSize: ({
    xs: 24,
    sm: 28,
    md: 32,
    lg: 36,
    xl: 40
  })[props.size || 'md']
}))

However, the template renders descriptions when either data exists OR the slot is used:

<span v-if="isSelectItem(item) && (get(item, props.descriptionKey as string) || !!slots['item-description'])">
  <slot name="item-description" :item="item" :index="index">
    {{ get(item, props.descriptionKey as string) }}
  </slot>
</span>

This mismatch caused the virtualizer to estimate 32px heights even when descriptions were being rendered (requiring 52px), resulting in overlapping items and broken scrolling.

Solution:

The fix detects whether any item will have a description rendered (either from data OR from the slot) and adjusts the estimateSize accordingly (look at code diff)

๐Ÿ“ Checklist

  • [ ] I have linked an issue or discussion.
  • [ ] I have updated the documentation accordingly.

aligu7 avatar Nov 01 '25 20:11 aligu7

npm i https://pkg.pr.new/@nuxt/ui@5363

commit: d6141cb

pkg-pr-new[bot] avatar Nov 01 '25 20:11 pkg-pr-new[bot]

Thanks for the suggestion! I explored implementing per-item estimateSize using a function.

The Challenge:

reka-ui's ComboboxVirtualizer currently only accepts estimateSize as a static number, not a function. The underlying @tanstack/vue-virtual DOES support estimateSize: (index: number) => number, but reka-ui wraps it and doesn't expose this API to users.

Here's the relevant code in reka-ui:

// In reka-ui/ListboxVirtualizer.vue line 67
estimateSize() {
  return props.estimateSize ?? 28  // Always returns a static number
}

aligu7 avatar Nov 05 '25 16:11 aligu7

Closing in favor of https://github.com/nuxt/ui/commit/56ae8e7199b8d5e3b5a169bd63c767724abfb582.

I've also opened a PR in Reka UI to be able to pass a function: https://github.com/unovue/reka-ui/pull/2288. The new getEstimateSize util is ready to handle it once released: https://github.com/nuxt/ui/blob/v4/src/runtime/utils/virtualizer.ts#L37-L42.

benjamincanac avatar Nov 17 '25 15:11 benjamincanac

Hey @benjamincanac ! Just noticed the release notes didnโ€™t include my name for this PR โ€” no worries at all, but if possible could it be added for attribution? Thank you!

aligu7 avatar Nov 18 '25 20:11 aligu7

@aligu7 What do you mean? This PR hasn't been merged ๐Ÿ˜ฌ

benjamincanac avatar Nov 18 '25 22:11 benjamincanac

@benjamincanac , after your changes in https://github.com/nuxt/ui/commit/56ae8e7 this issue hasn't been fixed completely, I know we are waiting for the Reka UI's response in https://github.com/unovue/reka-ui/pull/2288 but let's until then have a working solution from me perhaps that has been implemented above or something else. Because this issue hasn't been fixed yet, and you closed this PR.

aligu7 avatar Dec 02 '25 08:12 aligu7