Autocomplete: Compound parts
The Autocomplete’s default layout is a root, a trigger, a popup and a live region. To place things between them, such as a visible label or a summary, build the layout yourself from the compound parts inside Autocomplete.Provider.
Nothing selected.
import { useId, useState } from "react";
import { Autocomplete, useAutocompleteContext, type OptionValue } from "slotsmith/autocomplete";
import { topics, type Topic } from "../shared/options";
/** Anything inside the provider reads the picker's state through the context. */
function SelectionSummary() {
const { values, clear } = useAutocompleteContext<Topic>();
if (values.length === 0) return <p>Nothing selected.</p>;
return (
<p>
{values.length} selected{" "}
<button type="button" onClick={clear} style={{ background: "transparent" }}>
Clear
</button>
</p>
);
}
export default function CompoundParts() {
const labelId = useId();
const [topicIds, setTopicIds] = useState<OptionValue[]>([]);
return (
<Autocomplete.Provider multiple options={topics} optionDisabled={(topic) => !!topic.archived} value={topicIds} onChange={setTopicIds}>
<Autocomplete.Root style={{ display: "grid", gap: "0.5rem", maxWidth: 360 }}>
<label id={labelId}>Topics</label>
<Autocomplete.Trigger aria-labelledby={labelId} />
<Autocomplete.Popup />
<Autocomplete.LiveRegion />
<SelectionSummary />
</Autocomplete.Root>
</Autocomplete.Provider>
);
}export type Country = { code: string; name: string };
/** Sorted by name, as a picker shows them. */
export const countries: Country[] = [
{ code: "ar", name: "Argentina" },
{ code: "au", name: "Australia" },
{ code: "br", name: "Brazil" },
{ code: "ca", name: "Canada" },
{ code: "eg", name: "Egypt" },
{ code: "fr", name: "France" },
{ code: "de", name: "Germany" },
{ code: "in", name: "India" },
{ code: "jp", name: "Japan" },
{ code: "jo", name: "Jordan" },
{ code: "ke", name: "Kenya" },
{ code: "mx", name: "Mexico" },
{ code: "ma", name: "Morocco" },
{ code: "nl", name: "Netherlands" },
{ code: "no", name: "Norway" },
{ code: "pt", name: "Portugal" },
{ code: "sa", name: "Saudi Arabia" },
{ code: "za", name: "South Africa" },
{ code: "es", name: "Spain" },
{ code: "se", name: "Sweden" },
{ code: "tr", name: "Türkiye" },
{ code: "ae", name: "United Arab Emirates" },
{ code: "gb", name: "United Kingdom" },
{ code: "us", name: "United States" },
];
/** `id` and `label` are the names the autocomplete reads by default, so topics need no getters. */
export type Topic = { id: string; label: string; archived?: boolean };
export const topics: Topic[] = [
{ id: "accessibility", label: "Accessibility" },
{ id: "animation", label: "Animation" },
{ id: "caching", label: "Caching" },
{ id: "design-systems", label: "Design systems" },
{ id: "forms", label: "Forms" },
{ id: "i18n", label: "Internationalisation" },
{ id: "performance", label: "Performance" },
{ id: "rtl", label: "Right-to-left" },
{ id: "server-components", label: "Server components" },
{ id: "state", label: "State" },
{ id: "testing", label: "Testing" },
{ id: "typescript", label: "TypeScript" },
{ id: "legacy", label: "Legacy browsers", archived: true },
];
/** A country with the region it is listed under; a `disabled` one stays in the list but cannot be picked. */
export type RegionCountry = Country & { region: string; disabled?: boolean };
/** A short list grouped by region. Groups show in the order of their first option, so the regions come in this order. */
export const countriesByRegion: RegionCountry[] = [
{ code: "de", name: "Germany", region: "Europe" },
{ code: "fr", name: "France", region: "Europe" },
{ code: "es", name: "Spain", region: "Europe" },
{ code: "ge", name: "Georgia", region: "Europe" },
{ code: "dz", name: "Algeria", region: "Africa" },
{ code: "eg", name: "Egypt", region: "Africa" },
{ code: "ng", name: "Nigeria", region: "Africa" },
{ code: "ne", name: "Niger", region: "Africa", disabled: true },
{ code: "jp", name: "Japan", region: "Asia" },
{ code: "sg", name: "Singapore", region: "Asia" },
{ code: "sa", name: "Saudi Arabia", region: "Asia" },
{ code: "sy", name: "Syria", region: "Asia" },
];Pick a few topics: the line under the picker reads the selection from the context and clears it.
The parts
Autocomplete.Provider takes every Autocomplete prop and renders no markup of its own. Inside it:
Autocomplete.Root: the wrapper, carryingdata-open,data-disabledanddata-empty. A pointer press outside it closes the popup.Autocomplete.Trigger: the combobox with the value or tags, the clear button and the indicator. Name it here:aria-labeloraria-labelledbyon the trigger itself.Autocomplete.Popup: the positioned popup with the search box and the list. It renders nothing while closed.Autocomplete.LiveRegion: the result count for screen readers. The default layout includes it; in your own, add it, as the demo does.
For a popup laid out differently, compose it from Autocomplete.Search, Autocomplete.List, Autocomplete.Options (the option rows without the listbox) and Autocomplete.StatusRows (the loading, empty or error row).
These are layout parts, not slots: each still renders the slots inside it, so components replaces the elements while the parts decide where they go.
Reading the state
useAutocompleteContext() returns the picker’s state and actions to any component inside the provider: values, selected, open, query, options, status, and select, remove, clear and setOpen, among others. The demo’s summary line uses values and clear.
To wrap Autocomplete in a component of your own that takes the same props, splitAutocompleteProps(props) separates the provider’s props from the root’s DOM props.
The hook
useAutocomplete is the engine underneath, with no markup at all: the selection, the search, the highlight, the full keyboard path and the ARIA wiring, as prop getters (getRootProps, getTriggerProps, getSearchProps, getListProps, getOptionProps). Use it for a shape the component does not cover, such as an editable input as the trigger or a command palette. Positioning is separate: usePopupPosition places a popup against its trigger.