Autocomplete

A text input that suggests as you type. The value is whatever the reader typed, which is what separates it from a Select.

Accessibility

AutoComplete renders a real combobox, so most of the pattern arrives for free. Two things do not: the field has no name until you give it one, and nothing announces that suggestions appeared.

What it renders

The field is a real combobox, wired the way the pattern asks for. antuikit changes none of it, so the work left to you is naming, announcing and contrast.
  • The input carries role="combobox" (the role prop can override it), aria-autocomplete="list" and aria-haspopup="listbox".
  • aria-expanded tracks the popup. aria-controls, aria-owns and aria-activedescendant are set only while it is open, and point at <id>_list and <id>_list_<index>.
  • The popup list is role="listbox" and each row is role="option" with aria-selected and aria-disabled. A row whose label is a plain string also gets it as aria-label.
  • The input's autoComplete defaults to new-password, which keeps the browser's autofill panel from covering the suggestions. Do not overwrite it without a reason.
  • A group heading is not announced as a group. A { label, options } row renders as a plain div inside the listbox with no role of its own, so the rows under it do not say which group they came from. Put the distinction in the option text when it changes what the option means.

Naming the field

The combobox has no name unless you give it one, and a placeholder is not a name: it disappears the moment there is text, which is when a reader is most likely to ask what this field was.
  • A visible label wired with htmlFor is the best answer, and a Form.Item label does it for you.
  • Where the design has no room, pass aria-label or point aria-labelledby at the text that already says it. Both are forwarded to the input.
  • A custom children input takes its own naming with it. The combobox attributes are merged onto your element, but the label is yours: put id and aria-label on the child.
  • Say what the field expects, not what it does. "City" beats "Search", and neither should be the only hint that suggestions exist.

Announcing what arrived

Nothing in the component says how many suggestions there are. The component has a polite live region, but it only speaks the current value, while the field is focused and the popup is closed. The suggestion count is never announced.
  • Screen readers do report the active row as aria-activedescendant moves, so arrowing through the list is covered. What is missing is the moment the list appears, and the moment it changes under a query that was already typed.
  • Where the count matters, add your own aria-live="polite" region beside the field and write a short sentence into it when the results settle: "8 suggestions", "no matches". Update it after the debounce, not on every keystroke, or it talks over itself.
  • Zero results is silent twice over. notFoundContent is null in this mode, so the popup does not open and there is nothing to read. Your live region is the only thing that will say so.
  • Only the rows the list has rendered exist in the DOM, so the accessible listbox is a window rather than the whole set. Passing virtual explicitly narrows it further: a separate hidden listbox then holds the active row and its two neighbors, and the visible rows lose role="option". A short list is better served by virtual={false}.

Keyboard

  • Focus never leaves the input. Arrow keys move the active row by moving aria-activedescendant. The popup is not a tab stop and the rows are not focusable, as the combobox pattern asks.
  • Nothing is active until you arrow. defaultActiveFirstOption computes to false here, so Enter on an untouched popup submits the typed text rather than silently taking a suggestion. That is the right default for free text.
  • Enter takes the active row when there is one, and calls preventDefault while the popup is open, so it does not also submit the surrounding form. With the popup closed, Enter reaches the form as usual.
  • Escape closes the popup. It does not undo anything. Escape stops at the field only when it closed the popup, so one Escape dismisses the list and a second reaches whatever is around it (a modal, a drawer). Text written into the field by backfill stays written.
  • Tab moves on and leaves the typed text as the value. A reader who tabs away mid-list is telling you they meant what they typed.
  • disabled removes the field from the tab order entirely, so its label and its value become unreachable. Prefer readOnly for a value that is not editable yet.

The popup is a portal

The list is rendered at the end of document.body, nowhere near the field in the DOM. Nothing about that is broken: the combobox pattern is built for it, and aria-controls plus aria-activedescendant are what tie the two together.
  • Do not try to fix the reading order by moving the popup into the field's own container. A reader never arrives at the list by walking the document; it hears the active option from the input.
  • getPopupContainer is for clipping, not for semantics. Reach for it when the field sits in a scrolling or transformed ancestor and the popup drifts. antuikit sets no default.
  • The portal is also why a [&_.ant-select-item] descendant selector on the field does not reach a row. Style rows with classNames.popup.listItem.
  • Inside a modal, check that the popup is not clipped and that Escape still reaches the modal after the list has closed. It does, because only the Escape that closes the list stops at the field.

Contrast and motion

  • The active row is marked by background alone. Restyle it and you are restyling the only thing a sighted keyboard user has, so check it against the popup background, not against the page.
  • The selected row and the active row are two different states. A theme that paints them the same color loses the distinction the moment both apply to different rows.
  • status recolors the border and nothing else, so it is a color-only signal by construction. The message beside the field is what carries the meaning. Inside a Form.Item with hasFeedback, a feedback icon still shows.
  • Every color comes from the active theme, so contrast is a property of the theme rather than of the component. The shipped antuikit theme is checked for contrast, and a custom seed is yours to check.
  • The popup's open and close animation can be turned off wholesale with ThemeScope theme={{ token: { motion: false } }}. A list that appears and re-sorts under a typing reader is where reduced motion helps most.