Autocomplete: Search in the trigger
By default the Autocomplete’s trigger is a button and the search box sits in the popup. With searchIn="trigger" the search box is an input inside the trigger itself, and that input is the combobox: you type straight into the field, and focus never moves to the popup.
Country: jp. Topics: forms.
import { useId, useState } from "react";
import { Autocomplete, type OptionValue } from "slotsmith/autocomplete";
import { countries, topics, type Country } from "../shared/options";
export default function SearchInTrigger() {
const [country, setCountry] = useState<OptionValue | null>("jp");
const [topicIds, setTopicIds] = useState<OptionValue[]>(["forms"]);
const countryId = useId();
const topicsId = useId();
return (
<div style={{ display: "grid", gap: "1rem", maxWidth: 360 }}>
<div style={{ display: "grid", gap: "0.375rem" }}>
{/* The input is the combobox and takes the id, so a plain <label for> names it. */}
<label htmlFor={countryId}>Country</label>
<Autocomplete<Country>
id={countryId}
searchIn="trigger"
options={countries}
getOptionValue={(entry) => entry.code}
getOptionLabel={(entry) => entry.name}
value={country}
onChange={setCountry}
placeholder="Search countries"
/>
</div>
<div style={{ display: "grid", gap: "0.375rem" }}>
<label htmlFor={topicsId}>Topics</label>
{/* In multiple mode the tags sit before the input, on the same line. */}
<Autocomplete
id={topicsId}
searchIn="trigger"
multiple
options={topics}
optionDisabled={(topic) => !!topic.archived}
value={topicIds}
onChange={setTopicIds}
placeholder="Add topics"
/>
</div>
<p>
Country: {country ?? "none"}. Topics: {topicIds.length === 0 ? "none" : topicIds.join(", ")}.
</p>
</div>
);
}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" },
];Click into Country and type “ger”: the list opens on Germany. In Topics, type to add a topic, and press Backspace in the empty input to remove the last tag.
Popup or trigger
searchIn takes two values:
"popup", the default. The trigger is a button withrole="combobox", and opening it moves focus to a search box at the top of the popup. Good for a select-like field that shows the choice first."trigger". The trigger is a text field: an input inside it is the combobox, and it keeps focus while the list is open. Good for a field people type into, as in a form or a filter bar.
With searchable={false} there is nothing to type into, so searchIn is ignored and the trigger stays a button.
The input takes the id and the naming props (aria-label, aria-labelledby, aria-describedby and the like). Because it is a real <input>, a plain <label for> names it, as in the demo.
One value
In single mode the input shows the picked label while the list is closed.
- Focusing the input selects the label, so typing replaces it with the search.
- Leaving without a pick, with Esc, Tab or a click elsewhere, puts the label back.
- The
placeholdershows only while nothing is picked. - The clear button is hidden while you type a search.
Several values
In multiple mode the tags sit before the input, on the same line, and wrap when they fill it. As in popup mode, tags beyond maxTags (2 by default) collapse into +N. A pick clears the search and keeps the list open, so you can type the next one. Backspace in the empty input removes the last tag.
The field
The trigger becomes a field around the input. It is not a tab stop: a press anywhere on it moves focus to the input.
- The clear button stays in the tab order. It returns focus to the input.
- The chevron is the
Toggleslot: a button outside the tab order that opens and closes the list without taking focus. Thetogglelabel names it (“Show options”). - The input is the
TriggerInputslot, with its ownslotProps.triggerInput. - The trigger carries
data-search-in="trigger"for styling. TheSearchandValueparts do not render in this mode.
Keyboard
The input follows the WAI-ARIA editable combobox. It owns the caret, so Space, Home and End stay in the text.
| Keys | Action |
|---|---|
| A–Z | Type into the search. The list opens and narrows. |
| ↓↑ | Open the list on the first or the last option; with the list open, move the highlight. |
| Alt+↓ | Open the list without moving the highlight. |
| Alt+↑ | Close the list. |
| PgDnPgUp | With the list open: move the highlight ten options. |
| HomeEnd | Move the caret to the start or the end of the text. |
| Enter | Pick the highlighted option. With nothing highlighted and a create row offered, create. |
| Esc | Close the list and clear the search. In single mode the picked label comes back. With the list closed, clear the search. |
| Tab | Close the list, and move focus on as usual. |
| Backspace | In multiple mode, with the input empty, remove the last tag. |
While an input method is composing, as when typing Japanese or Chinese, Enter confirms the composition and picks nothing. The popup’s search box behaves the same way.
Controlling the search
searchQuery with onSearchChange works as in popup mode, with one difference: while searchQuery is not empty, the parent owns what the input shows, open or closed. The picked label comes back only once you set it to "". A close asks for that by calling onSearchChange(""), so store what it passes.