Select

One value or several, chosen from a list the field opens. Search, grouping, tags, custom option rendering and virtual scrolling.

Accessibility

Select is an ARIA combobox built from a div and an input. The pattern is complete, but it does not give you what a native element would.

It is a combobox, not a select

There is no <select> in this component. It is an ARIA combobox: a text input carrying role="combobox", aria-expanded, aria-haspopup="listbox", aria-autocomplete="list" and, while the popup is open, aria-controls and aria-activedescendantpointing into it. The popup is a role="listbox" whose rows are role="option" with aria-selected and aria-disabled; a group heading is role="presentation".
That is a complete and correct pattern, but it lacks three things a native element gives you. Know them before you choose Select over a Radio group.
  • No platform picker. A phone does not open its own wheel or sheet for this control. It opens a floating list inside the page, at the page's own font size.
  • No form control. Nothing is submitted with a surrounding <form>; the value lives in React state and reaches the server the way the rest of your state does.
  • No type-ahead. With search off, the inner input is readOnly and typing a letter does nothing at all. A native select would have jumped to the first matching option.

Labeling it

The combobox is an input, so the usual routes work. Every aria-* prop you pass to Select goes to that inner input, not the wrapper div, so aria-label lands where the role is.
  • Prefer a visible label. Inside Form.Item the wiring is done for you; outside one, give the Select an id and point a <label htmlFor> at it, or reach for aria-labelledby.
  • placeholder is not a label. It disappears the moment a value is picked, which is when a reader wants to know what the field was for.
  • The clear button is a real <button> with a name, but the name defaults to the English string "Clear", not to a locale entry. In a localized product pass allowClear={{ label: … }}.
  • An option's accessible name comes from its label, and only when that label is a string. An optionRender that returns markup leaves the row named by whatever text it happens to contain, so keep the primary text first and plain.

Keyboard

  • Enter or Space opens the popup. In a searchable Select, Space types a space instead, which is correct and is why the two modes feel different under the hands.
  • Up and Down move the active option; Enter or Tab commits it; Escape closes without committing. Tab committing as well as Enter surprises people who expect Tab to leave the value alone.
  • Type-ahead only exists as search. There is no first-letter jump. If a list is long enough that a reader would want one, turn showSearch on rather than leaving them to arrow.
  • Backspace removes the last tag in a multi mode, when the search box is empty, skipping disabled values. This matters more than it sounds: see the next section.
  • Nothing else is a tab stop. The whole control, popup included, is one stop, which is what the combobox pattern wants.

Announcing the selected set

While the control is focused it renders a visually hidden aria-live="polite" region listing the selected labels, so a change in a multi-select is spoken without moving focus. It caps at 50 values and then says , ..., which is a reasonable cap and also a reason not to build a control that holds hundreds.
Removing a tag is the gap. Each tag's remove control is a <span> carrying aria-hidden="true" and a click handler. It has no role, no name and no tab stop, so assistive technology cannot see it and the keyboard cannot reach it. The only keyboard route to removing a value is Backspace, which removes the last one.
  • Do not build a multi-select whose only way back out of a mid-list value is Backspacing through everything after it. Reopen the popup and deselect there, and make sure your optionRender keeps the selected state visible.
  • tagRender is the way out. Rendering a Tag with closable and onClose wired gives you a place for a named, focusable remove button that the default tag does not have.
  • maxTagCount collapses tags visually only. The live region still reads every selected label, so the reader is not told that some are hidden behind a + N tag.

The popup is portalled

By default the popup renders into document.body, nowhere near the trigger in the DOM. The pattern is built for that: aria-controls and aria-activedescendant tie the two together by id, so a screen reader follows the relationship rather than the document order. Three consequences follow.
  • Reading order is not visual order. A reader browsing the document linearly, rather than operating the combobox, meets the list at the end of the page. Nothing should depend on the popup's DOM position.
  • A descendant selector never reaches it. [&_.ant-select-item] on the field matches nothing. Style the popup with classNames.popup. It follows your theme wherever it renders.
  • Reach for getPopupContainer for layout, not for theme. Inside a scrolling panel or a Modal, pinning the popup to its own container is what keeps it scrolling and clipping with its trigger.

Virtual scrolling has a cost here

Virtual scrolling is on by default, and it changes what assistive technology can perceive. With it on, the rows you see carry no aria-selected; the real role="listbox" is a zero-size element holding only three options: the active one and its neighbors.
  • A reader arrowing through the list is told the option they are on, which works, and that is all they are told. They cannot survey the list, and a count of options is not available.
  • For a list a reader needs to browse rather than search, and short enough that it costs nothing, virtual={false} puts every option back into one real listbox with its own aria-selected.
  • For anything long, turn search on instead of turning virtual off. Filtering to a handful is kinder than arrowing through a thousand.

Disabled, contrast and motion

  • There is no readOnly prop. disabled is the only lock, and it lands on the inner input, which takes the whole control out of the tab order. A keyboard reader then cannot reach it to find out what it says. For a value that must be visible but not changeable, render it as text.
  • Disable individual options through the option's own disabled where you can. Those still appear in the list with aria-disabled, so the reader learns the option exists and is unavailable rather than that it does not exist.
  • status paints a border color and nothing else. Color alone is not an error message: put the reason in Form.Item's help text, where it is tied to the field by id.
  • Check the placeholder against the field background, not against the page. It is the lowest-contrast text this component paints and it is the one carrying the field's meaning until a value is picked.
  • The popup's open and close motion turns off with a theme that sets motion: false. Anything you animate yourself inside optionRender or popupRender belongs behind motion-safe:.