Skip to content

Conversation

@aligu7
Copy link

@aligu7 aligu7 commented Nov 1, 2025

πŸ”— 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)
  • 🐞 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.

…on slot

- Fix virtualizerProps to check for both description data and item-description slot
- Ensures correct item height estimation when using slots with virtualization
- Prevents UI issues like overlapping items or broken scrolling
@aligu7 aligu7 requested a review from benjamincanac as a code owner November 1, 2025 20:30
@github-actions github-actions bot added the v4 #4488 label Nov 1, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 1, 2025

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

commit: d6141cb

Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

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

Didn't think of that, nice catch! We probably have the issue in InputMenu and CommandPalette as well.

Maybe it would be easier to fix it using a function on estimateSize because not all items necessarily have a description 😬

@aligu7
Copy link
Author

aligu7 commented Nov 5, 2025

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
}

Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

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

This change is also needed in CommandPalette and InputMenu I guess.

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

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants