Vize

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.

SelectScrollButton

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.