Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Select

A common form control for choosing a predefined value in a dropdown.

Rendered by React 19.3.1

Loading React…
import { Select } from '@hamur/react/select'

const apples = [
  { label: 'Gala', value: 'gala' },
  { label: 'Fuji', value: 'fuji' },
  { label: 'Honeycrisp', value: 'honeycrisp' },
  { label: 'Granny Smith', value: 'granny-smith' },
  { label: 'Pink Lady', value: 'pink-lady' },
]

export default function SelectExample() {
  return (
    <Select.Root items={apples}>
      <div className="field">
        <Select.Label className="select-label">
          Apple
        </Select.Label>
        <Select.Trigger className="select">
          <Select.Value className="select-value" placeholder="Select apple" />
          <Select.Icon className="select-icon" />
        </Select.Trigger>
      </div>
      <Select.Portal>
        <Select.Positioner className="select-positioner" sideOffset={6}>
          <Select.Popup className="select-popup">
            <Select.ScrollUpArrow className="select-scroll-arrow" />
            <Select.List className="select-list">
              {apples.map(({ label, value }) => (
                <Select.Item key={value} value={value} className="select-item">
                  <Select.ItemIndicator
                    className="select-item-indicator"
                  >
                    ✓
                  </Select.ItemIndicator>
                  <Select.ItemText className="select-item-text">
                    {label}
                  </Select.ItemText>
                </Select.Item>
              ))}
            </Select.List>
            <Select.ScrollDownArrow className="select-scroll-arrow" />
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  )
}
1,234Tests passing in React · 0 failing
56.1 KiBSize
2.40 msOpen
1.50 msClose
88.0 msOpenMutations
41.0 msCloseMutations
3.07 MiBMemory
productdevbook and 40 upstream authors17 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Select.Root>
  <Select.Label />
  <Select.Trigger>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Portal>
    <Select.Positioner>
      <Select.Popup>
        <Select.ScrollUpArrow />
        <Select.List>
          <Select.Item>
            <Select.ItemIndicator />
            <Select.ItemText />
          </Select.Item>
        </Select.List>
        <Select.ScrollDownArrow />
      </Select.Popup>
    </Select.Positioner>
  </Select.Portal>
</Select.Root>

// Not in this example: Select.Arrow, Select.Backdrop, Select.Group, Select.GroupLabel, Select.Separator

Examples

Multiple

multiple keeps several values.

SelectMultiple.tsx
import { Select } from '@hamur/react/select'

const languages = {
  javascript: 'JavaScript',
  typescript: 'TypeScript',
  python: 'Python',
  java: 'Java',
  csharp: 'C#',
  php: 'PHP',
  cpp: 'C++',
  rust: 'Rust',
  go: 'Go',
  swift: 'Swift',
}

type Language = keyof typeof languages

const values = Object.keys(languages) as Language[]

function renderValue(value: Language[]) {
  if (value.length === 0) {
    return 'Select languages'
  }
  const firstLanguage = languages[value[0]]
  const additionalLanguages = value.length > 1 ? ` (+${value.length - 1} more)` : ''
  return firstLanguage + additionalLanguages
}

export default function SelectMultipleExample() {
  return (
    <Select.Root multiple defaultValue={['javascript', 'typescript']}>
      <div className="field">
        <Select.Label className="select-label">
          Languages
        </Select.Label>
        <Select.Trigger className="select">
          <Select.Value className="select-value">
            {renderValue}
          </Select.Value>
          <Select.Icon className="select-icon" />
        </Select.Trigger>
      </div>
      <Select.Portal>
        <Select.Positioner
          className="select-positioner"
          sideOffset={6}
          alignItemWithTrigger={false}
        >
          <Select.Popup className="select-popup select-list">
            {values.map((value) => (
              <Select.Item key={value} value={value} className="select-item">
                <Select.ItemIndicator className="select-item-indicator">
                  ✓
                </Select.ItemIndicator>
                <Select.ItemText className="select-item-text">
                  {languages[value]}
                </Select.ItemText>
              </Select.Item>
            ))}
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  )
}

Object Values

Items use objects as values; Value renders the chosen object's label.

SelectObjectValues.tsx
import { Select } from '@hamur/react/select'

interface ShippingMethod {
  id: string
  name: string
  duration: string
  price: string
}

const shippingMethods: ShippingMethod[] = [
  { id: 'standard', name: 'Standard', duration: 'Delivers in 4-6 business days', price: '$4.99' },
  { id: 'express', name: 'Express', duration: 'Delivers in 2-3 business days', price: '$9.99' },
  { id: 'overnight', name: 'Overnight', duration: 'Delivers next business day', price: '$19.99' },
]

export default function SelectObjectValuesExample() {
  return (
    <Select.Root
      defaultValue={shippingMethods[0]}
      itemToStringValue={(item: ShippingMethod) => item.id}
    >
      <div className="field">
        <Select.Label className="select-label">
          Shipping method
        </Select.Label>
        <Select.Trigger className="select select-tall">
          <Select.Value className="select-value">
            {(method: ShippingMethod) => (
              <span className="select-value-text">
                <span>{method.name}</span>
                <span className="select-value-secondary">
                  {method.duration} ({method.price})
                </span>
              </span>
            )}
          </Select.Value>
          <Select.Icon className="select-icon" />
        </Select.Trigger>
      </div>
      <Select.Portal>
        <Select.Positioner className="select-positioner" sideOffset={6}>
          <Select.Popup className="select-popup">
            <Select.ScrollUpArrow className="select-scroll-arrow" />
            <Select.List className="select-list">
              {shippingMethods.map((method) => (
                <Select.Item
                  key={method.id}
                  value={method}
                  className="select-item"
                >
                  <Select.ItemIndicator
                    className="select-item-indicator"
                  >
                    ✓
                  </Select.ItemIndicator>
                  <Select.ItemText
                    className="select-item-text select-value-text"
                  >
                    <span>{method.name}</span>
                    <span className="select-value-secondary">
                      {method.duration} ({method.price})
                    </span>
                  </Select.ItemText>
                </Select.Item>
              ))}
            </Select.List>
            <Select.ScrollDownArrow className="select-scroll-arrow" />
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  )
}

Grouped

Group and GroupLabel add headings inside the popup.

SelectGrouped.tsx
import * as React from 'react'
import { Select } from '@hamur/react/select'

const groupedProduce = [
  {
    value: 'Fruits',
    items: [
      { value: 'apple', label: 'Apple' },
      { value: 'banana', label: 'Banana' },
      { value: 'mango', label: 'Mango' },
      { value: 'kiwi', label: 'Kiwi' },
      { value: 'grape', label: 'Grape' },
      { value: 'orange', label: 'Orange' },
      { value: 'strawberry', label: 'Strawberry' },
      { value: 'watermelon', label: 'Watermelon' },
    ],
  },
  {
    value: 'Vegetables',
    items: [
      { value: 'broccoli', label: 'Broccoli' },
      { value: 'carrot', label: 'Carrot' },
      { value: 'cauliflower', label: 'Cauliflower' },
      { value: 'cucumber', label: 'Cucumber' },
      { value: 'kale', label: 'Kale' },
      { value: 'pepper', label: 'Bell pepper' },
      { value: 'spinach', label: 'Spinach' },
      { value: 'zucchini', label: 'Zucchini' },
    ],
  },
]

export default function SelectGroupedExample() {
  return (
    <Select.Root items={groupedProduce}>
      <div className="field">
        <Select.Label className="select-label">
          Produce
        </Select.Label>
        <Select.Trigger className="select">
          <Select.Value className="select-value" placeholder="Select produce" />
          <Select.Icon className="select-icon" />
        </Select.Trigger>
      </div>
      <Select.Portal>
        <Select.Positioner className="select-positioner" sideOffset={6}>
          <Select.Popup className="select-popup">
            <Select.ScrollUpArrow className="select-scroll-arrow" />
            <Select.List className="select-list">
              {groupedProduce.map((group, index) => (
                <React.Fragment key={group.value}>
                  <Select.Group className="select-group">
                    <Select.GroupLabel className="select-group-label">
                      {group.value}
                    </Select.GroupLabel>
                    {group.items.map((item) => (
                      <Select.Item
                        key={item.value}
                        value={item.value}
                        className="select-item"
                      >
                        <Select.ItemIndicator
                          className="select-item-indicator"
                        >
                          ✓
                        </Select.ItemIndicator>
                        <Select.ItemText className="select-item-text">
                          {item.label}
                        </Select.ItemText>
                      </Select.Item>
                    ))}
                  </Select.Group>
                  {index < groupedProduce.length - 1 ? (
                    <Select.Separator className="select-separator" />
                  ) : null}
                </React.Fragment>
              ))}
            </Select.List>
            <Select.ScrollDownArrow className="select-scroll-arrow" />
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  )
}

API reference

56 props across 19 parts, in React syntax. className, style, render are listed once, under Shared props.

ArrowSelect.Arrow

Displays an element positioned against the select popup anchor. Renders a <div>.

Arrow data attributes
Data attributeDescription
data-openPresent when the select popup is open.
data-closedPresent when the select popup is closed.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-uncenteredPresent when the select arrow is uncentered.

BackdropSelect.Backdrop

An overlay displayed beneath the select popup. Renders a <div>.

Backdrop data attributes
Data attributeDescription
data-openPresent when the select is open.
data-closedPresent when the select is closed.
data-starting-stylePresent when the select begins animating in.
data-ending-stylePresent when the select is animating out.

GroupSelect.Group

Groups related select items with the corresponding label. Renders a <div>.

GroupLabelSelect.GroupLabel

An accessible label that is automatically associated with its parent group. Renders a <div>.

IconSelect.Icon

An icon that indicates that the trigger button opens a select popup. Renders a <span>.

Icon data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popup is open.

ItemSelect.Item

An individual option in the select popup. Renders a <div>.

Item props
PropTypeDefault
labelSpecifies the text label to use when the item is matched during keyboard text navigation.string—
valueA unique value that identifies this select item.anynull
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
childrenReact.ReactNode—
Item data attributes
Data attributeDescription
data-selectedPresent when the select item is selected.
data-highlightedPresent when the select item is highlighted.
data-disabledPresent when the select item is disabled.

ItemIndicatorSelect.ItemIndicator

Indicates whether the select item is selected. Renders a <span>.

ItemIndicator props
PropTypeDefault
childrenReact.ReactNode—
keepMountedWhether to keep the HTML element in the DOM when the item is not selected.boolean—
ItemIndicator data attributes
Data attributeDescription
data-starting-stylePresent when the indicator begins animating in.
data-ending-stylePresent when the indicator is animating out.

ItemTextSelect.ItemText

A text label of the select item. Renders a <div>.

LabelSelect.Label

An accessible label that is automatically associated with the select trigger. Renders a <div>.

ListSelect.List

A container for the select items. Renders a <div>.

PopupSelect.Popup

A container for the select list. Renders a <div>.

Popup props
PropTypeDefault
finalFocusDetermines the element to focus when the select popup is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
childrenReact.ReactNode—
Popup data attributes
Data attributeDescription
data-openPresent when the select is open.
data-closedPresent when the select is closed.
data-starting-stylePresent when the select begins animating in.
data-ending-stylePresent when the select is animating out.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.

PortalSelect.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—

PositionerSelect.Positioner

Positions the select popup. Renders a <div>.

Positioner props
PropTypeDefault
alignItemWithTriggerWhether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text.booleantrue
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
Positioner data attributes
Data attributeDescription
data-openPresent when the select popup is open.
data-closedPresent when the select popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the trigger and the edge of the viewport.
--available-heightThe available height between the trigger and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.

RootSelect.Root

Groups all parts of the select. Doesn’t render its own element.

Root props
PropTypeDefault
nameIdentifies the field when a form is submitted.string—
defaultValueThe uncontrolled value of the select when it's initially rendered.SelectInputValue<Value, Multiple> | null—
valueThe value of the select.SelectInputValue<Value, Multiple> | null—
onValueChangeEvent handler called when the value of the select changes.((value: SelectOutputValue<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: SelectRootChangeEventDetails) => void)—
defaultOpenWhether the select popup is initially open.booleanfalse
openWhether the select popup is currently open.boolean—
onOpenChangeEvent handler called when the select popup is opened or closed.((open: boolean, eventDetails: SelectRootOpenChangeEventDetails) => void)—
highlightItemOnHoverWhether moving the pointer over items should highlight them.booleantrue
actionsRefA ref to imperative actions.React.RefObject<SelectRootActions | null>—
autoCompleteProvides a hint to the browser for autofill.string—
formIdentifies the form that owns the hidden input.string—
isItemEqualToValueCustom comparison logic used to determine if a select item value matches the current selected value.((itemValue: Value, value: Value) => boolean)—
itemToStringLabelWhen the item values are objects (<Select.Item value={object}>), this function converts the object value to a string representation for display in the trigger.((itemValue: Value) => string)—
itemToStringValueWhen the item values are objects (<Select.Item value={object}>), this function converts the object value to a string representation for form submission.((itemValue: Value) => string)—
itemsData structure of the items rendered in the select popup.readonly Group<any>[] | Record<string, React.ReactNode> | readonly { label: React.ReactNode; value: any; }[]—
modalDetermines if the select enters a modal state when open.booleantrue
multipleWhether multiple items can be selected.Multiplefalse
onOpenChangeCompleteEvent handler called after any animations complete when the select popup is opened or closed.((open: boolean) => void)—
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to choose a different option from the select popup.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
idThe id of the Select.string—
childrenReact.ReactNode—
isItemDisabledWhether the item at index is disabled, for items that are not mounted (a virtualized list).((itemValue: Value, index: number) => boolean)—

ScrollDownArrowSelect.ScrollDownArrow

An element that scrolls the select popup down when hovered. Does not render when using touch input. Renders a <div>.

ScrollDownArrow props
PropTypeDefault
keepMountedWhether to keep the HTML element in the DOM while the select popup is not scrollable.booleanfalse
ScrollDownArrow data attributes
Data attributeDescription
data-starting-stylePresent when the scroll arrow begins animating in.
data-ending-stylePresent when the scroll arrow is animating out.
data-directionIndicates the direction of the scroll arrow.
data-visiblePresent when the scroll arrow is visible.
data-sideIndicates which side the popup is positioned relative to the trigger.

ScrollUpArrowSelect.ScrollUpArrow

An element that scrolls the select popup up when hovered. Does not render when using touch input. Renders a <div>.

ScrollUpArrow props
PropTypeDefault
keepMountedWhether to keep the HTML element in the DOM while the select popup is not scrollable.booleanfalse
ScrollUpArrow data attributes
Data attributeDescription
data-starting-stylePresent when the scroll arrow begins animating in.
data-ending-stylePresent when the scroll arrow is animating out.
data-directionIndicates the direction of the scroll arrow.
data-visiblePresent when the scroll arrow is visible.
data-sideIndicates which side the popup is positioned relative to the trigger.

SeparatorSelect.Separator

A visual separator between items or groups. Renders a <div>.

Separator props
PropTypeDefault
orientationThe orientation of the separator.Orientation'horizontal'
Separator data attributes
Data attributeDescription
data-orientationIndicates the orientation of the separator.

TriggerSelect.Trigger

A button that opens the select popup. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
disabledWhether the component should ignore user interaction.boolean—
childrenReact.ReactNode—
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding select is open.
data-pressedPresent when the trigger is pressed.
data-disabledPresent when the select is disabled.
data-readonlyPresent when the select is readonly.
data-popup-sideIndicates which side the corresponding popup is positioned relative to its anchor.
data-requiredPresent when the select is required.
data-validPresent when the select is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the select is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the select has been touched (when wrapped in Field.Root).
data-dirtyPresent when the select's value has changed (when wrapped in Field.Root).
data-filledPresent when the select has a value (when wrapped in Field.Root).
data-focusedPresent when the select trigger is focused (when wrapped in Field.Root).
data-placeholderPresent when the select doesn't have a value.

ValueSelect.Value

A text label of the currently selected item. Renders a <span>.

Value props
PropTypeDefault
placeholderThe placeholder value to display when no value is selected.React.ReactNode—
childrenAccepts a function that returns a ReactNode to format the selected value.React.ReactNode | ((value: any) => React.ReactNode)—
Value data attributes
Data attributeDescription
data-placeholderPresent when the select doesn't have a value.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowDownEnterSpaceOpens the popupTrigger
On the closed trigger, selects the next item whose text starts with what was typedTrigger
ArrowDownArrowUpMoves the highlight one item at a time and stops at the endsList
HomeEndMoves the highlight to the first or last itemList
In the open popup, highlights the next item whose text starts with what was typedList
EnterSelects the highlighted item and closes the popupItem
EscapeCloses the popup without changing the value and keeps focus on the triggerPopup
TabCloses the popup without changing the value and moves onPopup

Accessibility

What the parts set
PartSetsSo that
Triggerrole="combobox", aria-haspopup="listbox", aria-expanded, aria-controlsa screen reader says the button picks from a list and whether it is open
Triggeraria-labelledby to Label, aria-readonly, aria-requiredthe control is read with its name and constraints
Listrole="listbox", aria-multiselectable with multiplethe items are announced as options, and whether several can be chosen
Itemrole="option", aria-selectedeach option is read with its selected state
Grouprole="group", aria-labelledby to its GroupLabelgrouped options are read under their heading
Iconaria-hidden="true"the decorative chevron is not read
ScrollUpArrowaria-hidden="true"the scroll arrows are pointer aids only
ScrollDownArrowaria-hidden="true"the scroll arrows are pointer aids only
Backdroprole="presentation"the overlay is not read

Focus order

  1. Trigger is the tab stop.
  2. Opening moves focus to the selected item; arrow keys move it.
  3. Escape returns focus to Trigger; Tab closes the popup and moves on.