Select
Accessible, unstyled custom Select: a select-only combobox with a typed listbox popup.
|
|
| Package |
@vizejs/ui/select |
| Maturity |
stable |
| Own the source |
vize lib pull select |
| Requires |
collection, composite-navigation, context, controllable-state, dismissable-layer, id, portal, positioner, presence, typeahead, virtualizer |
| Aliases |
select, custom select, dropdown select, select-only combobox, picker |
| Covers |
WAI-ARIA select-only combobox pattern, Radix/Reka UI Select, React Aria Select, Headless UI Listbox, Ark UI Select |
Usage
import { Select, SelectRoot, SelectContent, SelectGroup, SelectItem, SelectItemIndicator, SelectLabel, SelectScrollButton, SelectSeparator, SelectTrigger, SelectValue, SelectViewport, SelectVirtualizer } from "@vizejs/ui/select";
Or copy the source into your project with vize lib pull select (see Source Distribution).
API
Select<T, Multiple extends boolean = false>
Source: src/families/selection/select/select-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
SelectModelValue<T, Multiple> |
undefined |
Controlled selection: T | null in single mode, readonly T[] when multiple is true. undefined selects uncontrolled behavior. |
defaultValue |
SelectModelValue<T, Multiple> |
undefined |
Initial selection for uncontrolled use and the value restored by reset. |
multiple |
Multiple |
false |
Allow selecting several options. The literal type of this prop decides the model type. |
items |
readonly T[] |
undefined |
Every option value, in display order. Optional for compound use; enables typed inference, closed-state labels, and virtualized navigation. |
by |
SelectBy<T> |
undefined |
Compare values by a property key or with a custom equality function. |
itemText |
(value: T) => string |
undefined |
Human-readable text for a value, used by SelectValue, typeahead, and SSR labels. |
itemDisabled |
(value: T) => boolean |
undefined |
Disable individual values declared through items. |
formValue |
(value: T) => string |
undefined |
Serialize a value for native form submission. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
disabled |
boolean |
false |
Disable the trigger, options, and form submission. |
required |
boolean |
false |
Require a selection for native constraint validation. |
name |
string |
undefined |
Form control name used by the hidden native select. |
form |
string |
undefined |
Id of the form owner when the select renders outside its <form>. |
autocomplete |
string |
undefined |
Native autofill hint forwarded to the hidden select. |
placeholder |
string |
undefined |
Text shown by SelectValue while nothing is selected. |
loop |
boolean |
false |
Wrap arrow-key navigation at the first and last option. |
typeaheadTimeout |
number |
500 |
Idle time before buffered typeahead starts a new query. |
selectOnTab |
boolean |
false |
Commit the highlighted option when Tab closes a single-selection popup (APG select-only combobox). |
closeOnSelect |
boolean |
undefined |
Close the popup after an option is chosen. undefined closes in single mode only. |
dir |
SelectDirection |
"ltr" |
Reading direction published to parts. |
ariaInvalid |
SelectAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: SelectModelValue<T, Multiple>] |
Fired when the selection requests a new controlled value (T | null or readonly T[]). |
change |
[ value: SelectModelValue<T, Multiple>, previous: SelectModelValue<T, Multiple>, nativeEvent: Event, ] |
Fired after a user interaction changes the selection: next value, previous value, and the native event. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
open-change |
[open: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request with the next state, previous state, and triggering event. |
Slots
| Slot |
Slot props |
Description |
default |
SelectSlotState<T> |
Compound Select parts. Receives the current selection and open state. |
Exposed
| Member |
Type |
Description |
clear |
— |
|
deselect |
— |
|
disabled |
— |
|
focus |
— |
|
id |
— |
|
invalid |
— |
|
listboxId |
— |
|
open |
— |
|
required |
— |
|
reset |
— |
|
select |
— |
|
selected |
— |
|
selectedText |
— |
|
selectionMode |
— |
|
setOpen |
— |
|
state |
— |
|
triggerId |
— |
|
SelectRoot<T, Multiple extends boolean = false>
Source: src/families/selection/select/select-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
SelectModelValue<T, Multiple> |
undefined |
Controlled selection: T | null in single mode, readonly T[] when multiple is true. undefined selects uncontrolled behavior. |
defaultValue |
SelectModelValue<T, Multiple> |
undefined |
Initial selection for uncontrolled use and the value restored by reset. |
multiple |
Multiple |
false |
Allow selecting several options. The literal type of this prop decides the model type. |
items |
readonly T[] |
undefined |
Every option value, in display order. Optional for compound use; enables typed inference, closed-state labels, and virtualized navigation. |
by |
SelectBy<T> |
undefined |
Compare values by a property key or with a custom equality function. |
itemText |
(value: T) => string |
undefined |
Human-readable text for a value, used by SelectValue, typeahead, and SSR labels. |
itemDisabled |
(value: T) => boolean |
undefined |
Disable individual values declared through items. |
formValue |
(value: T) => string |
undefined |
Serialize a value for native form submission. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
disabled |
boolean |
false |
Disable the trigger, options, and form submission. |
required |
boolean |
false |
Require a selection for native constraint validation. |
name |
string |
undefined |
Form control name used by the hidden native select. |
form |
string |
undefined |
Id of the form owner when the select renders outside its <form>. |
autocomplete |
string |
undefined |
Native autofill hint forwarded to the hidden select. |
placeholder |
string |
undefined |
Text shown by SelectValue while nothing is selected. |
loop |
boolean |
false |
Wrap arrow-key navigation at the first and last option. |
typeaheadTimeout |
number |
500 |
Idle time before buffered typeahead starts a new query. |
selectOnTab |
boolean |
false |
Commit the highlighted option when Tab closes a single-selection popup (APG select-only combobox). |
closeOnSelect |
boolean |
undefined |
Close the popup after an option is chosen. undefined closes in single mode only. |
dir |
SelectDirection |
"ltr" |
Reading direction published to parts. |
ariaInvalid |
SelectAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: SelectModelValue<T, Multiple>] |
Fired when the selection requests a new controlled value (T | null or readonly T[]). |
change |
[ value: SelectModelValue<T, Multiple>, previous: SelectModelValue<T, Multiple>, nativeEvent: Event, ] |
Fired after a user interaction changes the selection: next value, previous value, and the native event. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
open-change |
[open: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request with the next state, previous state, and triggering event. |
Slots
| Slot |
Slot props |
Description |
default |
SelectSlotState<T> |
Compound Select parts. Receives the current selection and open state. |
Exposed
| Member |
Type |
Description |
selected |
readonly T[] |
Selected values in selection order. |
selectedText |
readonly string[] |
Display text for each selected value. |
open |
boolean |
Whether the popup is open. |
disabled |
boolean |
Whether the select is disabled. |
required |
boolean |
Whether a selection is required. |
invalid |
boolean |
Whether the select is invalid (prop or failed native validation). |
selectionMode |
SelectSelectionMode |
Single or multiple selection. |
state |
SelectState |
Stable state token. |
id |
string |
Root-owned base id. |
triggerId |
string |
Id of the trigger element. |
listboxId |
string |
Id of the listbox element. |
setOpen |
(open: boolean, event?: Event | null) => boolean |
Request a specific open state. |
select |
(value: T) => boolean |
Select a value (toggle in multiple mode) and report whether it changed. |
deselect |
(value: T) => boolean |
Deselect a value and report whether it changed. |
clear |
() => boolean |
Clear the selection and report whether it changed. |
reset |
() => boolean |
Restore defaultValue and report whether it changed. |
focus |
(options?: FocusOptions) => void |
Move focus to the trigger. |
SelectContent
Source: src/families/selection/select/select-content.vue
Props
| Prop |
Type |
Default |
Description |
position |
SelectPosition |
"popper" |
popper anchors below the trigger; item-aligned overlays the selected option on the trigger. |
placement |
Placement |
"bottom-start" |
Preferred popper placement before collision handling. |
strategy |
PositionerStrategy |
"fixed" |
CSS positioning mode published on the floating host. |
offset |
number |
4 |
Gap between trigger and listbox in popper mode. |
collisionPadding |
number |
8 |
Viewport padding the listbox should not cross. |
flip |
boolean |
true |
Flip to the opposite side when the preferred side overflows more (popper mode). |
shift |
boolean |
true |
Shift the listbox back into the viewport. |
size |
boolean |
true |
Publish available-size CSS variables and constrain the host. |
hide |
boolean |
true |
Hide when the trigger scrolls out of view. |
forceMount |
boolean |
false |
Keep options mounted (hidden) while closed so labels and typeahead work before first open. |
to |
string | HTMLElement |
"body" |
CSS selector or element the popup is moved into. |
portalDisabled |
boolean |
false |
Render in place instead of teleporting. |
defer |
boolean |
true |
Keep content in place until the portal target exists, avoiding SSR mismatch. |
closeOnEscape |
boolean |
true |
Let Escape close the popup. |
closeOnPointerDownOutside |
boolean |
true |
Let an outside pointer-down close the popup. |
ariaLabel |
string |
undefined |
Accessible name for the listbox when the trigger label is not enough. |
ariaLabelledby |
string |
undefined |
Space-separated ids labelling the listbox. Defaults to the trigger. |
Events
| Event |
Payload |
Description |
escape-key-down |
[event: SelectEscapeKeyDownEvent] |
Fired before Escape closes the popup. Call preventDefault() to keep it open. |
pointer-down-outside |
[event: SelectPointerDownOutsideEvent] |
Fired before an outside pointer-down closes the popup. Call preventDefault() to keep it open. |
dismiss |
[event: SelectDismissEvent] |
Fired after an unprevented dismissal request closes the popup. |
Slots
| Slot |
Slot props |
Description |
default |
SelectContentSlotState |
Options, groups, separators, viewport, and scroll buttons. Receives open state and placement. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered listbox element. |
SelectGroup
Source: src/families/selection/select/select-group.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned group id. null and undefined select a deterministic fallback. |
ariaLabel |
string |
undefined |
Accessible group name used instead of a SelectLabel. |
Slots
| Slot |
Slot props |
Description |
default |
— |
A SelectLabel followed by the group's options. |
SelectItem<T>
Source: src/families/selection/select/select-item.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned option id. null and undefined select a deterministic fallback. |
value (required) |
T |
required |
Option value compared with the root selection through by. |
disabled |
boolean |
false |
Disable this option. |
textValue |
string |
undefined |
Explicit label for typeahead and SelectValue. undefined extracts the option text. |
index |
number |
undefined |
Absolute index inside the root items, required inside SelectVirtualizer. |
ariaLabel |
string |
undefined |
Accessible name when the option text is not enough. |
Slots
| Slot |
Slot props |
Description |
default |
SelectItemSlotState<T> |
Option content. Receives selected, active, and disabled state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered option element. |
value |
T |
Option value. |
selected |
boolean |
Whether this option is selected. |
active |
boolean |
Whether this option is highlighted. |
select |
() => boolean |
Choose this option as if clicked. |
SelectItemIndicator
Source: src/families/selection/select/select-item-indicator.vue
Props
| Prop |
Type |
Default |
Description |
forceMount |
boolean |
false |
Render while the option is unselected too, publishing data-state for CSS transitions. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly selected: boolean } |
Indicator content such as a check mark. Receives the option selection state. |
SelectLabel
Source: src/families/selection/select/select-label.vue
Slots
| Slot |
Slot props |
Description |
default |
— |
Visible group heading text. |
Source: src/families/selection/select/select-scroll-button.vue
Props
| Prop |
Type |
Default |
Description |
direction (required) |
SelectScrollDirection |
required |
Scroll direction driven while hovered or pressed. |
step |
number |
32 |
Pixels scrolled per tick. |
interval |
number |
50 |
Milliseconds between ticks while hovered or pressed. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly direction: SelectScrollDirection } |
Decorative arrow content. Receives the scroll direction. |
Exposed
| Member |
Type |
Description |
scrollOnce |
— |
|
update |
— |
|
visible |
— |
|
SelectSeparator
Source: src/families/selection/select/select-separator.vue
Slots
| Slot |
Slot props |
Description |
default |
— |
Optional decorative separator content. |
SelectTrigger
Source: src/families/selection/select/select-trigger.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name when no visible label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids of visible labels for the select. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the select. |
ariaErrormessage |
string |
undefined |
Id of the validation message announced while invalid. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly open: boolean; readonly disabled: boolean; readonly empty: boolean; } |
Trigger content, usually SelectValue plus an icon. Receives open and selection state. |
Exposed
| Member |
Type |
Description |
element |
HTMLButtonElement | null |
Rendered trigger button. |
focus |
(options?: FocusOptions) => void |
Move focus to the trigger. |
SelectValue
Source: src/families/selection/select/select-value.vue
Props
| Prop |
Type |
Default |
Description |
separator |
string |
", " |
Text placed between labels when several values are selected. |
Slots
| Slot |
Slot props |
Description |
default |
SelectValueSlotState<unknown> |
Custom selected-value rendering. Receives the selected values, their text, and the placeholder. |
placeholder |
SelectValueSlotState<unknown> |
Custom placeholder rendering shown while nothing is selected. |
SelectViewport
Source: src/families/selection/select/select-viewport.vue
Slots
| Slot |
Slot props |
Description |
default |
— |
Scrollable options region. Scroll buttons and SelectVirtualizer observe this element. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
SelectVirtualizer<T>
Source: src/families/selection/select/select-virtualizer.vue
Props
| Prop |
Type |
Default |
Description |
items (required) |
readonly T[] |
required |
Every option value in display order; only the visible window is rendered. |
estimateItemSize |
number |
32 |
Estimated option height in CSS pixels used until options are measured. |
overscan |
number |
4 |
Options rendered beyond each edge of the visible window. |
initialViewportHeight |
number |
256 |
Viewport height assumed for server rendering and before measurement. |
getItemKey |
(item: T, index: number) => string | number |
undefined |
Stable render key for an option. Defaults to its index. |
Slots
| Slot |
Slot props |
Description |
default |
SelectVirtualItemSlotState<T> |
Render one SelectItem with :value="item" and :index="index". |
Exposed
| Member |
Type |
Description |
scrollToIndex |
— |
|
totalSize |
— |
|
virtualItems |
— |
|
Behavior
Contract
Select is a headless, strongly typed custom select built on the WAI-ARIA APG
select-only combobox pattern. select-root.vue is generic over the option
value type T and a Multiple literal: omitting multiple (or passing
false) makes the model T | null; multiple: true makes it readonly T[].
Values may be any type; by compares them by property key or custom equality.
select-trigger.vue renders a native <button role="combobox"> that keeps DOM
focus for the whole interaction and exposes the highlighted option through
aria-activedescendant. select-content.vue renders the role="listbox"
popup through Portal, Presence, Positioner, and a dismissable layer; options
(select-item.vue) register with the shared collection registry and composite
navigation used by Listbox. select-value.vue renders the selected label or
placeholder, select-item-indicator.vue a selection mark,
select-group.vue/select-label.vue labelled groups, select-separator.vue a
decorative divider, select-viewport.vue the scroll container,
select-scroll-button.vue hover-to-scroll affordances, and
select-virtualizer.vue a windowed option list for large collections.
The root always renders a hidden, aria-hidden native <select> that mirrors
the selection for form submission, required validation, form= association,
form reset, and autofill. No CSS is shipped; every part publishes
data-vize-ui, data-state, and state-specific data attributes.
Public Surface
| Surface |
Contract |
SelectRoot props |
id, modelValue, defaultValue, multiple, items, by, itemText, itemDisabled, formValue, open, defaultOpen, disabled, required, name, form, autocomplete, placeholder, loop, typeaheadTimeout, selectOnTab, closeOnSelect, dir, ariaInvalid |
SelectRoot emits |
update:modelValue(value), change(value, previous, nativeEvent), update:open(open), open-change(open, previous, nativeEvent) |
SelectRoot expose |
id, triggerId, listboxId, selected, selectedText, open, disabled, required, invalid, selectionMode, state, setOpen, select, deselect, clear, reset, focus |
SelectTrigger props |
ariaLabel, ariaLabelledby, ariaDescribedby, ariaErrormessage |
SelectValue props/slots |
separator; slots default(state), placeholder(state) |
SelectContent props |
position, placement, strategy, offset, collisionPadding, flip, shift, size, hide, forceMount, to, portalDisabled, defer, closeOnEscape, closeOnPointerDownOutside, ariaLabel, ariaLabelledby |
SelectContent emits |
escape-key-down(event), pointer-down-outside(event), dismiss(event) |
SelectItem props |
id, value, disabled, textValue, index, ariaLabel |
SelectVirtualizer props |
items, estimateItemSize, overscan, initialViewportHeight, getItemKey |
| Parts |
root, trigger, value, content-host, content, viewport, item, item-indicator, group, label, separator, scroll-button, virtualizer, native |
| Root data attributes |
data-vize-ui="select", data-state, data-selection-mode, data-disabled, data-required, data-invalid, data-empty |
| Item data attributes |
data-vize-ui="select-item", data-state="checked|unchecked", data-highlighted, data-disabled, data-index |
Normative Behavior
| # |
State |
Input |
Outcome |
Proven by |
| S1 |
closed, empty |
render |
trigger is a button with role="combobox", aria-haspopup="listbox", aria-expanded="false", no aria-controls; value shows the placeholder |
renders select-only combobox semantics with placeholder and closed popup |
| S2 |
closed |
click trigger |
opens, wires aria-controls to the listbox, highlights the selected option through aria-activedescendant |
click opens the listbox, highlights the selected option, and click selects |
| S3 |
open, single |
click enabled option |
selects it, emits update:modelValue and change(value, previous, event), closes, value shows its label |
click opens the listbox, highlights the selected option, and click selects |
| S4 |
open |
pointer move over option |
highlights enabled options only; disabled options neither highlight nor select; option pointer-down never steals trigger focus |
pointer movement highlights enabled options without selecting |
| S5 |
open, multiple |
click option |
toggles it, emits a frozen readonly T[], keeps the popup open |
multiple selection toggles values, keeps the popup open, and emits arrays |
| S6 |
template multiple |
attribute form ("") |
resolves to multiple mode |
the attribute form of multiple selects multiple mode |
| S7 |
controlled |
selection or open request |
emits the request and renders the parent value; by matches structurally equal values |
controlled value and open state wait for the parent |
| S8 |
by comparator |
render / open |
comparator decides selection; labels fall back to label/name/title/text fields |
a comparator \by` and object text fallbacks resolve selection and labels` |
| S9 |
disabled root |
ArrowDown / click |
trigger is natively disabled and never opens |
disabled roots do not open and disabled options are skipped |
| S10 |
groups |
render |
role="group" labelled by its SelectLabel or ariaLabel; separators are aria-hidden |
groups wire labels and separators stay out of the accessibility tree |
| S11 |
any |
exposed API |
select, deselect, clear, reset, setOpen, focus share the same state |
exposed methods drive the same selection and open state |
| S12 |
no provider |
mount a part |
throws VIZE_UI_CONTEXT_MISSING |
parts require a Select provider |
| K1 |
closed |
ArrowDown / ArrowUp / Enter / Space |
prevents default, opens, highlights the selected option (or the first) |
closed "ArrowDown" opens and highlights the selected option and siblings |
| K2 |
closed, empty |
ArrowDown |
highlights the first enabled option |
closed ArrowDown without a selection highlights the first option |
| K3 |
closed |
Home / End |
opens on the first / last enabled option |
closed Home and End open on the first and last enabled option |
| K4 |
closed / open |
printable character |
opens and runs buffered typeahead; Space extends a pending query instead of selecting |
closed printable characters open and run typeahead |
| K5 |
open |
ArrowDown / ArrowUp / Home / End / Page |
moves the highlight, skipping disabled options, stopping at edges |
open arrows skip disabled options and stop at the edges without loop |
| K6 |
open, loop |
ArrowDown at end / ArrowUp at start |
wraps |
loop wraps arrow navigation |
| K7 |
open, single |
Enter / Space |
selects the highlighted option, closes, clears aria-activedescendant; change carries the key event |
open "Enter" selects the highlighted option and closes and open " " … |
| K8 |
open, multiple |
Enter |
toggles and stays open |
Enter in multiple mode toggles and keeps the popup open |
| K9 |
open / closed |
Escape |
closes without selecting (prevented); ignored while closed so ancestors can react |
Escape closes without selecting and is left alone while closed |
| K10 |
open |
Tab |
closes without preventing focus movement; commits only with selectOnTab |
Tab closes without committing unless selectOnTab is set |
| K11 |
open, single |
Alt+ArrowUp |
commits the highlighted option and closes |
Alt+ArrowUp commits the highlighted option and closes |
| K12 |
any |
IME composition / Ctrl/Meta chords |
ignored |
IME composition and modified keys are ignored |
| F1 |
named |
form submission |
hidden native select submits formValue(value) or the by key |
submits the serialized \by` key through a hidden native select` |
| F2 |
required, empty |
checkValidity() |
invalid; the invalid event focuses the trigger and marks aria-invalid/data-invalid |
empty single selections submit an empty value and fail required validation |
| F3 |
multiple |
form submission |
one selected native option per value |
multiple selections submit one entry per value with a custom serializer |
| F4 |
disabled |
form submission |
native control is disabled and excluded |
disabled selects are excluded from submission |
| F5 |
form attribute |
render outside the form |
associates with the owner form |
the form attribute associates a select rendered outside its form |
| F6 |
autofill / form reset |
native change / reset |
autofill maps back to items; reset restores defaultValue |
form reset restores the default selection |
| O1 |
open |
outside pointer-down |
dismissable layer closes the popup; the trigger is a branch and does not dismiss |
an outside pointer-down dismisses the popup through the dismissable layer, pointer-down on the trigger is a layer branch and does not dismiss |
| O2 |
open |
default portal |
listbox teleports to body, stays wired by id, and is removed on unmount |
the popup teleports to body by default and stays wired to the trigger |
| O3 |
item-aligned |
open |
selected (or first) option is centred over the trigger |
item-aligned mode publishes its strategy and centres the anchor option on the trigger |
| O4 |
scrollable viewport |
hover / leave scroll button |
buttons are hidden unless scrolling is possible and scroll by step |
scroll buttons appear only while the viewport can scroll and scroll on hover |
| V1 |
virtualized |
open / End / Home / PageDown / typeahead |
only a window renders; navigation and typeahead reach every option |
virtualized lists render a window and navigate across the whole collection |
| V2 |
virtualized, selected |
open |
scrolls to and highlights a selected option outside the window |
virtualized lists open on a selected option outside the initial window |
Options rendered by SelectVirtualizer carry an absolute index; the
collection orders options by index, so any option mixed into the same popup
(for example a pinned option before the virtual window) must pass index too.
SSR
Ids derive from the deterministic-id primitive. Closed and open trees, and a
virtual window laid out against initialViewportHeight, render byte-identical
markup across isolated requests and hydrate without warnings or node
replacement (renders byte-identical closed Select markup with SSR labels and form mirror, renders byte-identical open Select markup in place before the portal hydrates, hydrates closed and open Selects without mismatches,
server-renders and hydrates a deterministic virtual window). Pass itemText
so closed triggers render the right label before any option has mounted.
Scroll buttons measure after mount, so they render hidden in server markup.