Skip to content

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

InputsReact

Cascader

A button that opens a hierarchy as columns side by side, one column per level, to pick an item deep in it, such as a city under its country and continent.

Rendered by React 19.3.1

Loading React…
import { Cascader } from '@hamur/react/cascader'

const labels: Record<string, string> = {
  asia: 'Asia',
  japan: 'Japan',
  tokyo: 'Tokyo',
  osaka: 'Osaka',
  kyoto: 'Kyoto',
  korea: 'Korea',
  seoul: 'Seoul',
  busan: 'Busan',
  europe: 'Europe',
  france: 'France',
  paris: 'Paris',
  lyon: 'Lyon',
  germany: 'Germany',
  berlin: 'Berlin',
  munich: 'Munich',
  americas: 'Americas',
  canada: 'Canada',
  toronto: 'Toronto',
  vancouver: 'Vancouver',
  brazil: 'Brazil',
  'sao-paulo': 'São Paulo',
  rio: 'Rio de Janeiro',
}
const children: Record<string, string[]> = {
  asia: ['japan', 'korea'],
  japan: ['tokyo', 'osaka', 'kyoto'],
  korea: ['seoul', 'busan'],
  europe: ['france', 'germany'],
  france: ['paris', 'lyon'],
  germany: ['berlin', 'munich'],
  americas: ['canada', 'brazil'],
  canada: ['toronto', 'vancouver'],
  brazil: ['sao-paulo', 'rio'],
}
const roots = ['asia', 'europe', 'americas']

export default function CascaderExample() {
  return (
    <Cascader.Root
      items={roots}
      getItemChildren={(value: string) => children[value]}
      itemToStringLabel={(value: string) => labels[value]}
    >
      <div className="field">
        <Cascader.Label className="select-label">
          Location
        </Cascader.Label>
        <div className="cascader-control">
          <Cascader.Trigger className="select">
            <Cascader.Value
              className="select-value cascader-value"
              placeholder="Choose a city"
            />
            <Cascader.Icon className="select-icon" />
          </Cascader.Trigger>
          <Cascader.Clear className="cascader-clear" aria-label="Clear">
            <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </Cascader.Clear>
        </div>
      </div>
      <Cascader.Portal>
        <Cascader.Positioner className="select-positioner" sideOffset={6}>
          <Cascader.Popup className="select-popup cascader-popup">
            <Cascader.Columns className="cascader-columns" aria-label="Places">
              {(column) => (
                <Cascader.Column
                  key={column.level}
                  column={column}
                  className="cascader-column"
                >
                  {(value: string) => (
                    <Cascader.Item
                      key={value}
                      value={value}
                      className="cascader-item"
                    >
                      <Cascader.ItemText className="cascader-item-text" />
                      <Cascader.ItemIndicator
                        className="cascader-item-indicator"
                      >
                        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                          <path d="M3 8.5l3 3 7-7" />
                        </svg>
                      </Cascader.ItemIndicator>
                      <Cascader.ItemIcon className="cascader-item-icon">
                        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                          <path d="M6 4l4 4-4 4" />
                        </svg>
                      </Cascader.ItemIcon>
                    </Cascader.Item>
                  )}
                </Cascader.Column>
              )}
            </Cascader.Columns>
          </Cascader.Popup>
        </Cascader.Positioner>
      </Cascader.Portal>
    </Cascader.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

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:

<Cascader.Root>
  <Cascader.Label />
  <Cascader.Trigger>
    <Cascader.Value />
    <Cascader.Icon />
  </Cascader.Trigger>
  <Cascader.Clear />
  <Cascader.Portal>
    <Cascader.Positioner>
      <Cascader.Popup>
        <Cascader.Columns>
          <Cascader.Column>
            <Cascader.Item>
              <Cascader.ItemText />
              <Cascader.ItemIndicator />
              <Cascader.ItemIcon />
            </Cascader.Item>
          </Cascader.Column>
        </Cascader.Columns>
      </Cascader.Popup>
    </Cascader.Positioner>
  </Cascader.Portal>
</Cascader.Root>

// Not in this example: Cascader.Arrow, Cascader.Backdrop, Cascader.Empty, Cascader.Input, Cascader.Status

Examples

Multiple

multiple with cascade and cascadeValue="parents": checking Europe checks its countries and reports one value, unchecking one of them leaves Europe mixed.

CascaderMultiple.tsx
import * as React from 'react'
import { Cascader } from '@hamur/react/cascader'

const labels: Record<string, string> = {
  americas: 'Americas',
  canada: 'Canada',
  mexico: 'Mexico',
  brazil: 'Brazil',
  europe: 'Europe',
  france: 'France',
  germany: 'Germany',
  spain: 'Spain',
  asia: 'Asia',
  japan: 'Japan',
  korea: 'Korea',
}
const children: Record<string, string[]> = {
  americas: ['canada', 'mexico', 'brazil'],
  europe: ['france', 'germany', 'spain'],
  asia: ['japan', 'korea'],
}
const roots = ['americas', 'europe', 'asia']

export default function CascaderMultipleExample() {
  const [value, setValue] = React.useState<string[]>(['france'])
  return (
    <div className="stack">
      {/* With cascade, checking a region checks its countries; "parents" keeps a whole region as one value. */}
      <Cascader.Root
        items={roots}
        getItemChildren={(item: string) => children[item]}
        itemToStringLabel={(item: string) => labels[item]}
        multiple
        cascade
        cascadeValue="parents"
        value={value}
        onValueChange={(next: string[]) => setValue(next)}
      >
        <div className="field">
          <Cascader.Label className="select-label">
            Markets
          </Cascader.Label>
          <Cascader.Trigger className="select">
            <Cascader.Value
              className="select-value cascader-value"
              placeholder="Choose markets"
            />
            <Cascader.Icon className="select-icon" />
          </Cascader.Trigger>
        </div>
        <Cascader.Portal>
          <Cascader.Positioner className="select-positioner" sideOffset={6}>
            <Cascader.Popup className="select-popup cascader-popup">
              <Cascader.Columns
                className="cascader-columns"
                aria-label="Markets"
              >
                {(column) => (
                  <Cascader.Column
                    key={column.level}
                    column={column}
                    className="cascader-column"
                  >
                    {(item: string) => (
                      <Cascader.Item
                        key={item}
                        value={item}
                        className="cascader-item"
                      >
                        <Cascader.ItemIndicator
                          keepMounted
                          className="tree-check"
                        >
                          <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
                            <path className="tree-check-mark" d="M3 8.5l3 3 7-7" />
                            <path className="tree-check-dash" d="M4 8h8" />
                          </svg>
                        </Cascader.ItemIndicator>
                        <Cascader.ItemText className="cascader-item-text" />
                        <Cascader.ItemIcon className="cascader-item-icon">
                          <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                            <path d="M6 4l4 4-4 4" />
                          </svg>
                        </Cascader.ItemIcon>
                      </Cascader.Item>
                    )}
                  </Cascader.Column>
                )}
              </Cascader.Columns>
            </Cascader.Popup>
          </Cascader.Positioner>
        </Cascader.Portal>
      </Cascader.Root>
      <p className="muted">Value: {value.length ? value.join(', ') : '(none)'}</p>
    </div>
  )
}

An Input at the top of the popup: typing swaps the columns for one column of the matching cities shown with their paths, and "No cities found" shows when nothing matches.

CascaderSearch.tsx
import { Cascader } from '@hamur/react/cascader'

const labels: Record<string, string> = {
  asia: 'Asia',
  japan: 'Japan',
  tokyo: 'Tokyo',
  osaka: 'Osaka',
  kyoto: 'Kyoto',
  korea: 'Korea',
  seoul: 'Seoul',
  busan: 'Busan',
  europe: 'Europe',
  france: 'France',
  paris: 'Paris',
  lyon: 'Lyon',
  germany: 'Germany',
  berlin: 'Berlin',
  munich: 'Munich',
  americas: 'Americas',
  canada: 'Canada',
  toronto: 'Toronto',
  vancouver: 'Vancouver',
  brazil: 'Brazil',
  'sao-paulo': 'São Paulo',
  rio: 'Rio de Janeiro',
}
const children: Record<string, string[]> = {
  asia: ['japan', 'korea'],
  japan: ['tokyo', 'osaka', 'kyoto'],
  korea: ['seoul', 'busan'],
  europe: ['france', 'germany'],
  france: ['paris', 'lyon'],
  germany: ['berlin', 'munich'],
  americas: ['canada', 'brazil'],
  canada: ['toronto', 'vancouver'],
  brazil: ['sao-paulo', 'rio'],
}
const roots = ['asia', 'europe', 'americas']

export default function CascaderSearchExample() {
  return (
    // Typing replaces the columns with one list of the cities whose path matches, shown as paths.
    <Cascader.Root
      items={roots}
      getItemChildren={(value: string) => children[value]}
      itemToStringLabel={(value: string) => labels[value]}
    >
      <div className="field">
        <Cascader.Label className="select-label">
          Destination
        </Cascader.Label>
        <Cascader.Trigger className="select">
          <Cascader.Value
            className="select-value cascader-value"
            placeholder="Choose a city"
          />
          <Cascader.Icon className="select-icon" />
        </Cascader.Trigger>
      </div>
      <Cascader.Portal>
        <Cascader.Positioner className="select-positioner" sideOffset={6}>
          <Cascader.Popup className="select-popup cascader-popup">
            <Cascader.Input
              className="input cascader-input"
              placeholder="Search cities"
              aria-label="Search cities"
            />
            <Cascader.Columns className="cascader-columns" aria-label="Places">
              {(column) => (
                <Cascader.Column
                  key={column.results ? 'results' : column.level}
                  column={column}
                  className="cascader-column"
                >
                  {(value: string) => (
                    <Cascader.Item
                      key={value}
                      value={value}
                      className="cascader-item"
                    >
                      <Cascader.ItemText className="cascader-item-text" />
                      <Cascader.ItemIndicator
                        className="cascader-item-indicator"
                      >
                        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                          <path d="M3 8.5l3 3 7-7" />
                        </svg>
                      </Cascader.ItemIndicator>
                      <Cascader.ItemIcon className="cascader-item-icon">
                        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                          <path d="M6 4l4 4-4 4" />
                        </svg>
                      </Cascader.ItemIcon>
                    </Cascader.Item>
                  )}
                </Cascader.Column>
              )}
            </Cascader.Columns>
            <Cascader.Empty className="cascader-empty">
              No cities found
            </Cascader.Empty>
          </Cascader.Popup>
        </Cascader.Positioner>
      </Cascader.Portal>
    </Cascader.Root>
  )
}

Lazy

Inline columns whose children come from loadChildren: a highlighted region shows its column loading, then its offices.

CascaderLazy.tsx
import * as React from 'react'
import { Cascader } from '@hamur/react/cascader'

const labels: Record<string, string> = {
  europe: 'Europe',
  france: 'France',
  paris: 'Paris',
  lyon: 'Lyon',
  germany: 'Germany',
  berlin: 'Berlin',
  asia: 'Asia',
  japan: 'Japan',
  tokyo: 'Tokyo',
  osaka: 'Osaka',
  antarctica: 'Antarctica',
}
// What the server would answer; the columns ask for a parent's children when it is highlighted.
const server: Record<string, string[]> = {
  europe: ['france', 'germany'],
  france: ['paris', 'lyon'],
  germany: ['berlin'],
  asia: ['japan'],
  japan: ['tokyo', 'osaka'],
}
const roots = ['europe', 'asia', 'antarctica']

function fetchChildren(value: string, signal: AbortSignal) {
  return new Promise<string[]>((resolve, reject) => {
    const timer = setTimeout(() => resolve(server[value]), 600)
    signal.addEventListener('abort', () => {
      clearTimeout(timer)
      reject(signal.reason)
    })
  })
}

export default function CascaderLazyExample() {
  const [loaded, setLoaded] = React.useState<Record<string, string[]>>({})
  const [value, setValue] = React.useState<string | null>(null)
  const loadChildren = async (item: string, { signal }: { signal: AbortSignal }) => {
    const items = await fetchChildren(item, signal)
    setLoaded((previous) => ({ ...previous, [item]: items }))
    return items
  }
  return (
    <div className="stack">
      {/* No Trigger and no Popup: the columns are the control. null marks children not loaded yet. */}
      <Cascader.Root
        items={roots}
        getItemChildren={(item: string) => loaded[item] ?? (server[item] ? null : undefined)}
        itemToStringLabel={(item: string) => labels[item]}
        loadChildren={loadChildren}
        value={value}
        onValueChange={(next: string | null) => setValue(next)}
      >
        <Cascader.Label className="select-label">
          Office
        </Cascader.Label>
        <Cascader.Columns className="cascader-columns inline">
          {(column) => (
            <Cascader.Column
              key={column.level}
              column={column}
              className="cascader-column"
            >
              {(item: string) => (
                <Cascader.Item key={item} value={item} className="cascader-item">
                  <Cascader.ItemText className="cascader-item-text" />
                  <Cascader.ItemIndicator
                    className="cascader-item-indicator"
                  >
                    <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                      <path d="M3 8.5l3 3 7-7" />
                    </svg>
                  </Cascader.ItemIndicator>
                  <Cascader.ItemIcon className="cascader-item-icon">
                    <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                      <path d="M6 4l4 4-4 4" />
                    </svg>
                  </Cascader.ItemIcon>
                </Cascader.Item>
              )}
            </Cascader.Column>
          )}
        </Cascader.Columns>
      </Cascader.Root>
      <p className="muted">Office: {value === null ? '(none)' : labels[value]}</p>
    </div>
  )
}

API reference

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

ArrowCascader.Arrow

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

Arrow data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the 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 popover arrow is uncentered.

BackdropCascader.Backdrop

An overlay displayed beneath the popover. Renders a <div>.

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

ClearCascader.Clear

Empties the value. Place it outside the trigger. Renders nothing while the value is empty. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

ColumnCascader.Column

One column: the first level, the children of an item on the open path, or the search results. Renders a <div>.

Column props
PropTypeDefault
columnrequiredThe column to render, as Cascader.Columns hands it over.CascaderColumn<any>—
childrenThe items: a node, or a function that renders each item of the column.React.ReactNode | ((item: any, index: number) => React.ReactNode)—

ColumnsCascader.Columns

The tree: the columns of the open path side by side, or the search results, followed by its status region. Without Cascader.Popup around it, it is the control itself. Renders a <div>.

Columns props
PropTypeDefault
childrenThe columns: a node, or a function that renders each column of the open path.React.ReactNode | ((column: CascaderColumn<any>) => React.ReactNode)—

EmptyCascader.Empty

Content shown while the search has no results, or the first column has no items. Renders nothing otherwise. Renders a <div>.

IconCascader.Icon

An icon that shows the trigger opens a popup. Hidden from assistive technology. Renders a <span>.

InputCascader.Input

A text input that searches the cascader's paths. It keeps focus while the keys move the highlight through the columns, named by aria-activedescendant. Renders an <input>.

ItemCascader.Item

An item of a column. A parent's press opens its column; a selectable item's press chooses it. Renders a <div>.

Item props
PropTypeDefault
valuerequiredThe item, one of its column's items.any—
indexThe item's index in its column, when the column renders only some of its items.number—

ItemIconCascader.ItemIcon

A parent's chevron, rendered for parents only. Hidden from assistive technology. Renders a <span>.

ItemIndicatorCascader.ItemIndicator

Shows whether the item is selected or checked. A press toggles the item with multiple. Renders a <span>.

ItemIndicator props
PropTypeDefault
keepMountedWhether to keep the indicator in the DOM while the item is neither selected nor checked.booleanfalse

ItemTextCascader.ItemText

The item's text: its label, or its path in the search results. Renders a <span>.

LabelCascader.Label

A label that names the trigger, or the columns when they are inline. A press focuses the trigger (the highlighted item inline). Renders a <div>.

PopupCascader.Popup

The container of the columns and the optional Input. Focus moves to the Input, else to the highlighted item, when it opens, and returns to the trigger when it closes. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the popover is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the popover is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—

PortalCascader.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
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—

PositionerCascader.Positioner

Positions the popup against the trigger; it moves as columns are added. Renders a <div>.

Positioner props
PropTypeDefault
alignHow to align the popup relative to the specified side.Align'start'
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
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
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—

RootCascader.Root

Groups all parts of the cascader: a trigger that opens columns of a hierarchy in a popup, or the columns alone, inline., only the form inputs. Doesn’t render its own element.

Root props
PropTypeDefault
childrenReact.ReactNode—
idThe id of the field control, for the hidden input.string—
itemsrequiredThe items of the first column.readonly Value[]—
getItemChildrenThe children of an item: an array (a parent), null (a parent not loaded) or undefined((item: Value) => readonly Value[] | null | undefined)—
valueThe selected item, or items with multiple.CascaderInputValue<Value, Multiple> | null—
defaultValueThe selected item, or items with multiple, when initially rendered.CascaderInputValue<Value, Multiple> | nullnull
onValueChangeEvent handler called when the value changes.((value: CascaderOutputValue<Value, Multiple>, eventDetails: CascaderRootChangeEventDetails) => void)—
multipleWhether several items can be selected.Multiplefalse
cascadeWith multiple: checking an item checks its enabled descendants.booleanfalse
cascadeValueWhat the value holds under cascade: 'all', 'parents' or 'leaves'.TreeCascadeValue'all'
disallowEmptySelectionWhether the last selected item of a multiple cascader cannot be deselected.booleanfalse
allowParentSelectionWhether parents can be chosen as well as leaves.booleanfalse
isItemSelectableWhether an item can be chosen.((item: Value) => boolean)—
isItemDisabledWhether an item is disabled; its descendants are too.((item: Value) => boolean)—
itemToStringLabelConverts an item to its text: ItemText, typeahead, the path text and values not in the data.((item: Value) => string)—
itemToStringValueConverts an item to a string for form submission.((item: Value) => string)—
isItemEqualToValueCustom comparison logic used to determine if an item matches the value.((itemValue: Value, value: Value) => boolean)—
loadChildrenCalled when the highlight lands on a parent whose children are null.((item: Value, options: { signal: AbortSignal; }) => Promise<void | readonly Value[]>)—
onLoadChildrenErrorCalled when loadChildren rejects.((item: Value, error: unknown) => void)—
highlightedValueThe highlighted item, which decides the open path.Value—
onHighlightChangeEvent handler called when the highlight moves.((value: Value) => void)—
onItemHighlightedVirtualized only: called when the highlight moves, with the item's index and level, so the((item: Value, eventDetails: CascaderHighlightDetails) => void)—
expandOnHoverWhether resting the pointer on an item moves the highlight there.booleanfalse
expandDelayHow long the pointer rests on an item before it is highlighted, in milliseconds.number100
loopFocusWhether Down on the last item of a column moves to the first, and Up on the first to the last.booleanfalse
separatorThe text between the items of a path.string' / '
inputValueThe text of Cascader.Input.string—
defaultInputValueThe text of Cascader.Input when initially rendered.string''
onInputValueChangeEvent handler called when the text of Cascader.Input changes.((value: string, eventDetails: CascaderRootInputValueChangeEventDetails) => void)—
filterWhether an item matches the query: it receives the item, the query and a function that gives((item: Value, query: string, itemToString: (item: Value) => string) => boolean) | null—
filteredItemsThe results to show while there is a query, in place of filtering.readonly Value[]—
virtualizedWhether the app renders only the visible items of each column.booleanfalse
openWhether the popup is open.boolean—
defaultOpenWhether the popup is initially open.booleanfalse
onOpenChangeEvent handler called when the popup opens or closes.((open: boolean, eventDetails: CascaderRootOpenChangeEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the popup opens or closes.((open: boolean) => void)—
actionsRefA ref to imperative actions.React.RefObject<PopoverRootActions | null>—
modalWhether the rest of the page is inert while the popup is open.boolean | "trap-focus"true
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the value.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden inputs.string—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
labelsThe texts of the status region.Partial<CascaderLabels>—

StatusCascader.Status

A polite status region for the app's text, such as the number of results. Renders a <div>.

TriggerCascader.Trigger

A button that opens the popup and shows the value. It is a combobox whose popup is a tree. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
idID of the trigger.string—

ValueCascader.Value

The text of the value: each value's path, joined by the separator. Renders a <span>.

Value props
PropTypeDefault
childrenWhat to show for the value instead of the path texts: a node, or a function of the value andReact.ReactNode | ((value: any, paths: any[][]) => React.ReactNode)—
placeholderWhat to show while nothing is selected.React.ReactNode—

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
EnterSpaceArrowDownArrowUpOpens the popup on the chosen item with its path openTrigger
ArrowDownArrowUpMoves within the column; the next column follows the highlightItem
HomeEndMoves to the first or last item of the columnItem
ArrowRightEnters the first child of a parentItem
ArrowLeftGoes back to the parent; the deeper columns closeItem
EnterSpaceChooses a leaf, closes the popup and returns focus to the TriggerItem
SpacemultipleChecks or unchecks the item and keeps the popup openItem
Move to the next item of the column whose text starts with themItem
EscapeCloses the popup and returns focus to the TriggerPopup
TabCloses the popup and chooses nothingPopup
ArrowDownArrowUpInputMoves the highlight through the results while focus stays in the InputInput
EnterInputChooses the highlighted result, closes the popup and clears the searchInput
EscapeInputClears the search first, then closes the popupInput

Accessibility

What the parts set
PartSetsSo that
Triggerrole="combobox", aria-haspopup="tree", aria-expanded and, while open, aria-controls; labelled by Labelthe button says it opens a tree, and whether it is open
Iconaria-hiddenthe chevron is for sight
Columnsrole="tree", named by Label or aria-label, with aria-multiselectable under multiplethe columns are read as one tree, not as several lists
Columnrole="group" for each column after the first, owned by the item it shows the children ofa screen reader hears each column as the children of its parent
Itemrole="treeitem", aria-level, aria-setsize, aria-posinset, aria-expanded on a parent, and aria-selected or, under multiple, aria-checkedeach item says where it is in the hierarchy and whether it is chosen
ItemIconaria-hiddenthe chevron of a parent is for sight; aria-expanded says it
Inputaria-controls the results and aria-activedescendant the highlighted onea screen reader follows the highlight while focus stays in the text
Statusa polite role="status" regiona load of children, its failure and an empty result are announced
Cleara button named "Clear" when it has no content, or by its text or aria-labelthe value is emptied from the keyboard

Focus order

  1. The Trigger is the Tab stop, then Clear when there is one. Nothing takes focus on mount.
  2. Opening moves focus to the chosen item with its path open, or the first item; with an Input, to the Input.
  3. Choosing a leaf or Escape returns focus to the Trigger; Tab closes the popup.
  4. Inline, the Columns are one Tab stop, on the highlighted item.