Skip to content

Command.Item and Calendar.Cell share the data-selected attribute, causing ~500ms style recalcs on hover when consumer CSS uses :has([data-selected]) #2044

@jose-manuel-silva

Description

@jose-manuel-silva

Describe the bug

Command.Item and Calendar.Cell / RangeCalendar.Cell both expose state via the data-selected attribute, but they mean different things:

  • Command.Item.data-selected = "this item is the currently active item" — flips on every pointer transition between items (via onpointermovesetValue, mutating aria-selected + data-selected on both the old and new item: 4 attribute mutations per transition).
  • Calendar.Cell.data-selected / RangeCalendar.Cell.data-selected = "this date is selected" — flips on click.

In isolation this is benign. But as soon as any consumer CSS contains a :has([data-selected]) rule (which is the natural Tailwind / CSS pattern for "highlight the cell when its day is selected"), Chromium registers data-selected document-wide for :has() invalidation. Every Command pointer transition then triggers Chromium to walk :has() invalidation across the page.

This reproduces today in production on https://www.shadcn-svelte.com/docs/components/combobox: hovering across the combobox items produces ~500 ms blocking UpdateLayoutTree events per item-to-item transition (≈7,000-element DOM). The combobox feels frozen while in use. The trigger is shadcn-svelte's [&:has([data-selected])]:bg-accent utility on the range-calendar cell, which ships in the global CSS bundle even on pages that don't render a calendar.

The slow path is in Chromium's :has() invalidator, but the fix that closes the entire class of problem lives in bits-ui: don't share data-selected between two unrelated primitives. Anyone pairing a Combobox/Command with a Calendar in the same app re-creates this bug regardless of CSS bundling.

Reproduction

Live, no setup required: https://www.shadcn-svelte.com/docs/components/combobox

  1. Click the combobox to open it.
  2. Move the cursor up and down across the framework items.
  3. Observe perceptible lag (~500 ms blocking) on each item transition.

Performance trace: each item-to-item transition fires one UpdateLayoutTree event of ~500 ms over ~7,000 elements. Cost scales linearly with surrounding DOM size, so the same Command in a smaller page is proportionally faster but not free.

Confirmed by isolated experiment: directly toggling data-selected on a Command.Item via JS on the same page (bypassing Command's handler) produces the same ~500 ms recalc. Stripping :has([data-selected]) from the page CSS drops it to ~1 ms. Renaming the attribute in Command source (verified locally) produces the same ~1 ms result without touching the CSS.

Suggested fix

Rename Command.Item's state attribute to something component-scoped — data-active, data-highlighted, or data-command-selected. Specifically in packages/bits-ui/src/lib/bits/command/command.svelte.ts around line 1480:

-"aria-selected": boolToStr(this.isSelected),
-"data-selected": boolToEmptyStrOrUndef(this.isSelected),
+"aria-selected": boolToStr(this.isSelected),
+"data-command-selected": boolToEmptyStrOrUndef(this.isSelected),

Note on the consumer-side fix

This can also be fixed at the consumer (shadcn-svelte) level by rewriting the [&:has([data-selected])]:bg-accent utility on the calendar cell to a direct data-selected:bg-accent against the cell — which already carries data-selected since bits-ui mirrors state to both Cell and Day. Verified locally; visual output identical, hover recalc cost drops from ~500 ms to ~1 ms.

I'm happy to open a parallel issue against shadcn-svelte covering that fix and a few related ones (the data-checked custom variant matching [data-state="checked"], two demo files using group-has-[[data-state=open]]/menu-item:) if it's useful — let me know.

The bits-ui-side rename is still worth doing because it closes the class of bug for any consumer pairing Command with a Calendar, regardless of CSS framework or how careful the consumer is with :has(). The shadcn-svelte fix is the workaround for this consumer; the rename is the structural fix.

(This was researched, verified and written by Opus 4.7, reviewed by me)

Reproduction

(above)

Logs

System Info

System:
    OS: macOS 15.7.3
    CPU: (10) arm64 Apple M4
    Memory: 135.97 MB / 24.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 25.6.1 - /opt/homebrew/bin/node
    npm: 11.9.0 - /opt/homebrew/bin/npm
    pnpm: 10.32.1 - /opt/homebrew/bin/pnpm
    bun: 1.3.13 - /opt/homebrew/bin/bun
    Deno: 2.7.4 - /opt/homebrew/bin/deno
  npmPackages:
    @sveltejs/kit: ^2.55.0 => 2.55.0
    bits-ui: ^2.16.3 => 2.16.3
    svelte: ^5.54.0 => 5.54.0

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageA maintainer needs to review this issue and label it appropriately

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions