Skip to content
Nizivo UI

Select

A single-choice list box with a button trigger, groups and separators, backed by a hidden native select so forms submit and reset as usual.

Client componentEngine: react-aria

Import

import { Select } from '@nizivo/react/select';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/field.css and @nizivo/react/css/popover.css
import '@nizivo/react/css/select.css';

Static or interactive

Client. It always ships React Aria's listbox and popup code and FieldScope (+72,949 B gzip under Turbopack for one Select on the Next.js fixture). NativeSelect is the static alternative when the browser's own picker fits. How to choose.

In a Field

Inside a Field, Select reads the Field the way Input does: the label and the selected value name the trigger, the description and the error describe it, and the Field's name, required, disabled and error state apply unless the Select sets its own (an explicit false wins). The Field's name reaches the hidden native <select>, so the value is in FormData.

A click on the label focuses the trigger, with its focus ring, and never opens the list: a native <label for> a button would press it. A required, empty Select stops the submit and takes focus, but shows no inline message yet (React Aria handles the hidden <select>'s invalid event), so pass error for it. The trigger is a button, which cannot carry aria-required, and the Field's * is hidden from screen readers, so put "required" in the label or the description too (the maintainer's rule for 0.1.0). A form reset puts the default value back in the trigger and in the submitted data.

The selected row

The selected row keeps a --nz-bg-brand-subtle tint, and its label turns Label/M Strong in --nz-text-primary. A check at its end (--nz-icon-brand) carries the state for readers who do not see the colour (WCAG 1.4.1). The check's 16 px column is reserved on every row, so selecting never moves the text. indicator on Select.Item replaces the check, and indicator={null} removes it. Figma draws no tint on the selected row; the code keeps it (the maintainer's decision of 2026-10-18, listed under Deviations).

The list

The list follows Figma's Select Menu (7356:406): --nz-radius-md, --nz-elevation-4, 6 px inside, and 4 px below the trigger (sideOffset on Select.Popup). Rows are 36 px of Label/M with 8 / 12 px padding and no gap between them, and the keyboard ring is drawn inside the row. Group labels are Label/S in --nz-text-secondary, padded 8 / 12 / 4 / 12. A separator has 4 px above and below. The Dropdown Menu (Menu) keeps its own look.

Text size on touch screens

Under a coarse pointer (a phone or a tablet), the trigger's value and placeholder are 16 px (Label/L) instead of 14 px (Label/M) at every size, matching NativeSelect, so swapping one for the other does not change the text. The trigger keeps its 32, 40 or 48 px height. The list's rows stay at 14 px and its group labels at 12 px: they are not text fields, so iOS does not zoom into them.

Examples

Basic

import type { JSX } from 'react';
import { Select } from '@nizivo/react/select';

export function SelectExample(): JSX.Element {
  return (
    <Select.Root defaultValue="30d" name="period" aria-label="Period">
      <Select.Trigger />
      <Select.Popup>
        <Select.Group>
          <Select.GroupLabel>Recent</Select.GroupLabel>
          <Select.Item value="7d">Last 7 days</Select.Item>
          <Select.Item value="30d">Last 30 days</Select.Item>
        </Select.Group>
        <Select.Separator />
        <Select.Item value="all" disabled>
          All time
        </Select.Item>
      </Select.Popup>
    </Select.Root>
  );
}

Field

In a Field: the label and description name and describe the trigger, the Field's name reaches the hidden native <select>, and a click on the label focuses the trigger (ring shown) without opening the list. The selected row shows the check (indicator replaces it; null removes it).

Applies to every chart on this page.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Select } from '@nizivo/react/select';

export function SelectFieldExample(): JSX.Element {
  return (
    <Field
      label="Reporting period"
      description="Applies to every chart on this page."
      name="period"
    >
      <Select.Root defaultValue="30d">
        <Select.Trigger />
        <Select.Popup>
          <Select.Item value="7d">Last 7 days</Select.Item>
          <Select.Item value="30d">Last 30 days</Select.Item>
          <Select.Item value="90d">Last 90 days</Select.Item>
        </Select.Popup>
      </Select.Root>
    </Field>
  );
}

Open

Opened on the server: the popup still renders nothing until hydration (§12.2 SSR).

Source only

It opens on page load (defaultOpen), which would move focus on this page, so it is shown as source only.
import type { JSX } from 'react';
import { Select } from '@nizivo/react/select';

export function SelectOpenExample(): JSX.Element {
  return (
    <Select.Root defaultOpen placeholder="Choose a period" aria-label="Period" size="sm" invalid>
      <Select.Trigger />
      <Select.Popup>
        <Select.Item value="7d">Last 7 days</Select.Item>
      </Select.Popup>
    </Select.Root>
  );
}

Parts

Use the parts through the namespace (Select.Root) or by their flat names (SelectRoot); both work from Server Components.

Select.Root

Client component Renders data-slot="select" with the class nz-select. Props: SelectRootProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
aria-labelstring–
aria-labelledbystring–

Names the trigger together with the value. Inside a Field, the Field label id goes here (§16.1).

childrenrequiredReactNode–
classNamestring–
defaultOpenboolean–
defaultValuestring | null–
disabledboolean–
invalidboolean–

An error message is presented (owned by Field, never by the engine). Inside a Field, the Field sets it.

namestring–

With name, a hidden native <select> carries the value into FormData. Inside a Field, the Field's name, required, disabled and presented error apply unless set here (an explicit false wins), and a click on its label focuses the trigger without opening the listbox.

onOpenChange(open: boolean, details: ChangeDetails<OpenReason>) => void–
onValueChange(value: string | null, details: ChangeDetails<SelectValueReason>) => void–

details.reason is 'unknown' with React Aria, which reports no cause.

openboolean–
placeholderSelectLabels["placeholder"]–
requiredboolean–
sizeControlSize
"sm""md""lg"
'md'

32 / 40 / 48 px.

valuestring | null–

Select.Trigger

Client component Renders data-slot="select-trigger" with the class nz-select-trigger. Props: SelectTriggerProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
childrenReactNode–

Replaces the default Select.Value.

indicatorReactNode–

Replaces the chevron glyph.

leadingIconReactNode–
renderRenderProp<SelectTriggerState>–

Must render a <button> (a component that renders one is fine).

  • Also takes every attribute of <button> except children, disabled, type. (Omit<ComponentPropsWithRef<"button">, "children" | "disabled" | "type">)

render takes an element, or, in client code, a function of the props and this state (SelectTriggerState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
openrequiredboolean–
pressedrequiredboolean–

Select.Value

Client component Renders data-slot="select-value" with the class nz-select-value. Props: SelectValueProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
renderRenderProp<SelectValueState>–

Must render a <span>.

  • Also takes every attribute of <span> except children. (Omit<ComponentPropsWithRef<"span">, "children">)

render takes an element, or, in client code, a function of the props and this state (SelectValueState):

NameTypeDefaultDescription
placeholderrequiredboolean–

Select.Popup

Client component Renders data-slot="select-popup" with the class nz-select-popup. Props: SelectPopupProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
alignAlign
"start""center""end"
'start'
childrenrequiredReactNode–
classNamestring–
containerElement | null–

Portal container. Defaults to the parent overlay, else document.body (§12.1 L2).

side"top" | "bottom"'bottom'
sideOffsetnumber4

Distance from the trigger in px.

styleCSSProperties–

Select.Item

Client component Renders data-slot="select-item" with the class nz-select-item. Props: SelectItemProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
childrenrequiredReactNode–
disabledboolean–
indicatorReactNode–

Replaces the check shown at the end of the selected item; null shows none. Decorative.

renderRenderProp<SelectItemState>–

Must render a <div>.

textValuestring–

Typeahead text; defaults to children when it is a string.

valuerequiredstring–
  • Also takes every attribute of <div> except children, id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)

render takes an element, or, in client code, a function of the props and this state (SelectItemState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
highlightedrequiredboolean–
selectedrequiredboolean–

Select.Group

Client component Renders data-slot="select-group" with the class nz-select-group. Props: SelectGroupProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
renderRenderProp<NoState>–

Must render a <section>.

  • Also takes every attribute of <section> except id. (Omit<ComponentPropsWithRef<"section">, "id">)

render takes an element, or, in client code, a function of the props and no state.

Select.GroupLabel

Client component Renders data-slot="select-group-label" with the class nz-select-group-label. Props: SelectGroupLabelProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
renderRenderProp<NoState>–

Must render a <header>.

  • Also takes every attribute of <header> except id. (Omit<ComponentPropsWithRef<"header">, "id">)

render takes an element, or, in client code, a function of the props and no state.

Select.Separator

Client component Renders data-slot="select-separator" with the class nz-select-separator. Props: SelectSeparatorProps. Source: src/select/select.client.tsx.

NameTypeDefaultDescription
renderRenderProp<NoState>–

Must render a <div>.

  • Also takes every attribute of <div> except children. (Omit<ComponentPropsWithRef<"div">, "children">)

render takes an element, or, in client code, a function of the props and no state.

Other exports

SelectGroupLabelProps type

import type { SelectGroupLabelProps } from '@nizivo/react/select'

The props of Select.GroupLabel, listed above.

SelectGroupProps type

import type { SelectGroupProps } from '@nizivo/react/select'

The props of Select.Group, listed above.

SelectItemProps type

import type { SelectItemProps } from '@nizivo/react/select'

The props of Select.Item, listed above.

SelectItemState type

import type { SelectItemState } from '@nizivo/react/select'

The render state of Select.Item, listed above.

SelectLabels type

import type { SelectLabels } from '@nizivo/react/select'

Strings the engine renders (§9.3 rule 7). placeholder is passed to the engine. Engine-owned with React Aria (not overridable): the visually hidden "Dismiss" buttons in the popup.

NameTypeDefaultDescription
placeholderrequiredstring–

Shown while no value is chosen. React Aria's default is its localized "Select an item".

SelectPopupProps type

import type { SelectPopupProps } from '@nizivo/react/select'

The props of Select.Popup, listed above.

SelectPopupState type

import type { SelectPopupState } from '@nizivo/react/select'

NameTypeDefaultDescription
enteringrequiredboolean–
exitingrequiredboolean–
siderequiredSide
"top""right""bottom""left"
–
SelectRootProps type

import type { SelectRootProps } from '@nizivo/react/select'

The props of Select.Root, listed above.

SelectRootState type

import type { SelectRootState } from '@nizivo/react/select'

NameTypeDefaultDescription
disabledrequiredboolean–
openrequiredboolean–
SelectSeparatorProps type

import type { SelectSeparatorProps } from '@nizivo/react/select'

The props of Select.Separator, listed above.

SelectTriggerProps type

import type { SelectTriggerProps } from '@nizivo/react/select'

The props of Select.Trigger, listed above.

SelectTriggerState type

import type { SelectTriggerState } from '@nizivo/react/select'

The render state of Select.Trigger, listed above.

SelectValueProps type

import type { SelectValueProps } from '@nizivo/react/select'

The props of Select.Value, listed above.

SelectValueReason type

import type { SelectValueReason } from '@nizivo/react/select'

type SelectValueReason = "item-press" | "keyboard" | "typeahead" | "form-reset"

SelectValueState type

import type { SelectValueState } from '@nizivo/react/select'

The render state of Select.Value, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
select
.nz-select
Select.Root
root (no render)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-invalid (present or absent) ← invalid
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-open (present or absent) ← open
    Open
  • data-required (present or absent) ← required
    Mirrors the required attribute
  • data-size: "sm""md""lg" ← size
    The size prop; values in each component manifest
select-chevron
.nz-select-chevron
Select.Trigger
inner element (CSS only)
none
select-group
.nz-select-group
Select.Group
root (takes render)
none
select-group-label
.nz-select-group-label
Select.GroupLabel
root (takes render)
none
select-icon
.nz-select-icon
Select.Trigger
inner element (CSS only)
none
select-item
.nz-select-item
Select.Item
root (takes render); interactive, focus: engine; forced colours: highlight on [data-selected]
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-focus-inset (present or absent)
    a11y-floor opt-out: draw the focus outline inside the part (outline-offset -2px), for clipped scrollers such as DialogBody and table cells (§8.5)
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-highlighted (present or absent)
    Keyboard or virtual focus, or pointer hover
  • data-selected (present or absent)
    Selected
select-list
.nz-select-list
Select.Popup
inner element (CSS only)
none
select-popup
.nz-select-popup
Select.Popup
root (no render); forced colours: border
  • data-align: "start""center""end" ← align
    Resolved alignment
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-open (present or absent)
    Open
  • data-side: "top""right""bottom""left"
    Resolved physical side after flip and RTL; the side prop takes logical values
select-separator
.nz-select-separator
Select.Separator
root (takes render)
none
select-trigger
.nz-select-trigger
Select.Trigger
root (takes render); interactive, focus: self
  • data-disabled (present or absent)
    Mirrors the disabled attribute
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-invalid (present or absent)
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-open (present or absent)
    Open
  • data-pressed (present or absent)
    Pointer or keyboard press (native parts use :active)
  • data-size: "sm""md""lg"
    The size prop; values in each component manifest
select-value
.nz-select-value
Select.Value
root (takes render)
  • data-placeholder (present or absent)
    No value chosen

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-select-bg--nz-bg-surface
  • background on .nz-select-trigger
--nz-select-border
  • default: var(--nz-border-control)
  • default: var(--nz-border-subtle)
  • data-invalid: var(--nz-border-critical)
  • border on .nz-select-trigger
--nz-select-border-hover
  • default: var(--nz-border-control-hover)
  • data-invalid: var(--nz-border-critical)
  • border-color on .nz-select-trigger:hover:not([data-disabled], :disabled) @media (hover: hover)
--nz-select-height
  • default: var(--nz-size-control-md)
  • data-size="lg": var(--nz-size-control-lg)
  • data-size="sm": var(--nz-size-control-sm)
  • block-size on .nz-select-trigger
--nz-select-radius
  • default: var(--nz-radius-md)
  • data-size="sm": var(--nz-radius-sm)
  • border-radius on .nz-select-trigger

Layers: nz.components. Classes: .nz-select-chevron, .nz-select-group, .nz-select-group-label, .nz-select-icon, .nz-select-item, .nz-select-item-indicator, .nz-select-list, .nz-select-popup, .nz-select-separator, .nz-select-trigger, .nz-select-value. Runtime variables: --nz-available-height, --nz-transform-origin, --nz-trigger-width.

Tokens this stylesheet reads (44)

--nz-bg-brand-subtle, --nz-bg-disabled, --nz-bg-hover, --nz-bg-surface, --nz-bg-surface-raised, --nz-border-control, --nz-border-control-hover, --nz-border-critical, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fade, --nz-duration-fast, --nz-duration-spring, --nz-easing-enter, --nz-easing-exit, --nz-easing-spring, --nz-easing-standard, --nz-elevation-4, --nz-focus-halo, --nz-focus-halo-critical, --nz-font-label-l, --nz-font-label-m, --nz-font-label-m-strong, --nz-font-label-s, --nz-icon-brand, --nz-radius-md, --nz-radius-sm, --nz-scale-enter, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-sm, --nz-space-1, --nz-space-1-5, --nz-space-2, --nz-space-3, --nz-text-disabled, --nz-text-primary, --nz-text-secondary, --nz-text-tertiary, --nz-tracking-label-m, --nz-tracking-label-s, --nz-z-overlay

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • select-item: draws the focus outline when the engine reports keyboard focus, not on pointer hover.
  • select-trigger: draws the focus outline itself on keyboard focus.

In forced-colours mode these states keep a visible system colour: select-item[data-selected] (highlight), select-popup (border).

Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.

Pattern: WAI-ARIA Authoring Practices.

Keyboard

KeysAction
Enter, Space, ArrowDownOn the trigger, opens the list and focuses the selected option, else the first.
ArrowUpOn the trigger, opens the list and focuses the selected option, else the last.
ArrowLeft, ArrowRightOn the closed trigger, selects the previous or next option without opening the list.
Type a characterSelects (on the trigger) or focuses (in the list) the next option whose label starts with what you typed.
ArrowDown, ArrowUpIn the open list, moves focus to the next or previous option, skipping disabled ones.
Home, End, PageUp, PageDownIn the open list, moves focus to the first or last option, or a page at a time.
Enter, SpaceIn the open list, selects the focused option and closes the list.
EscapeCloses the list without changing the selection and returns focus to the trigger.
Tab, Shift+TabIn the open list, closes it without changing the selection and moves focus to the next or previous element after the trigger.

Source: react-aria-components 1.22.1 Select (react-aria 3.53.1 useSelect, useMenuTrigger and useSelectableCollection) through _engines/react-aria/select.client.tsx. apps/docs/scripts/smoke.ts checks ArrowDown opening on the selected option, ArrowDown and Enter choosing, Escape closing with focus back on the trigger, ArrowRight and ArrowLeft on the closed trigger, and typing on the trigger; the overlay lane (test/overlay/select.spec.tsx) checks Tab and Shift+Tab, which the adapter handles because React Aria keeps Tab inside its modal popover.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page