Primitives Forms
Autocomplete
A text input with a datalist: the browser renders and filters the suggestions and keeps any other text. The package adds a stylesheet and a contract. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<input list>,<datalist>,<option>,:user-invalid- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input list> + <datalist>
On this page
Example
<label for="autocomplete-team">Team</label>
<input
id="autocomplete-team"
name="team"
list="autocomplete-team-options"
autocomplete="off"
data-slean="autocomplete"
/>
<datalist id="autocomplete-team-options">
<option value="Engineering"></option>
<option value="Design"></option>
<option value="Operations"></option>
<option value="Growth"></option>
</datalist><script lang="ts">
// Nothing to import: the browser owns the field and the list. The stylesheet is optional.
import '@svelte-lean/styles/autocomplete.css';
let team = $state('');
</script>
<label for="autocomplete-team">Team</label>
<input
id="autocomplete-team"
name="team"
list="autocomplete-team-options"
autocomplete="off"
bind:value={team}
data-slean="autocomplete"
/>
<datalist id="autocomplete-team-options">
<option value="Engineering"></option>
<option value="Design"></option>
<option value="Operations"></option>
<option value="Growth"></option>
</datalist>Why this implementation exists
Suggestions under a text field are a combobox, and a combobox built in script needs a popup, filtering, an active option and the ARIA that ties them together. When the options are plain text and free text is allowed, <datalist> gives the same result with the browser doing all of it.
The trade is control: the browser draws the list, matches by its own rule and takes no CSS for it. For options with their own markup, or a value restricted to the list, use the Combobox.
The browser owns
- matching the typed text against the options and rendering the list
- the list’s keyboard: ArrowDown, ArrowUp, Enter and Escape
- free text: the value is whatever the field holds
- the combobox role of a text input with a list, and form participation
Svelte Lean owns
- autocomplete.css: the text box and the themed list indicator
- the contract
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/autocomplete.css';Point the input’s list at the datalist’s id. Each <option value> is a suggestion; the value is what the field receives.
autocomplete="off" asks the browser not to mix its history of past entries into the list.
Change the suggestions by rendering new <option> elements, for example from an {#each} over fetched results; the browser filters whatever is there.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <input list="<datalist id>" data-slean="autocomplete"> | – | yes | A text, search, email, url or tel input with a label and a name. |
| options | <datalist id="…"> with <option value="…"> | – | yes | Referenced by the input’s list attribute. Rendered by the browser; the list cannot be styled. |
Runtime profile
Tier 0: the field has no behavior module, and the Vite plugin maps autocomplete to no module. Matching, the list and its keyboard are the browser’s.
Accessibility contract
- HTML-AAM maps a text input with a
listattribute to the combobox role; no ARIA is added. - A label through
<label for>; a hint througharia-describedby. - The list’s keyboard (ArrowDown, ArrowUp, Enter, Escape) is the browser’s and differs slightly between browsers.
Keyboard
| Key | When | Result |
|---|---|---|
| characters | focus in the field | Types and filters the list (native; matching differs between browsers) |
| ArrowDown/ArrowUp | focus in the field | Opens the list and moves through the options (native) |
| Enter | an option highlighted | Writes the option’s value into the field (native) |
| Escape | list open | Closes the list and keeps the text (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<input list> | Baseline 2019 | A plain text input |
<datalist> | Not Baseline: matching, labels and whether a list appears differ by browser | A plain text input; the typed value is still submitted |
Without JavaScript
Fully functional: typing, choosing a suggestion and submitting need no script.
Server rendering
Static HTML: the input and the datalist, with its options, render on the server.
Before hydration
Nothing is attached; the field and its list work before and after hydration alike.
Styling
autocomplete.css draws the text box from the tokens and dims the list indicator some browsers draw inside the field. The list is rendered outside the page and takes no CSS. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-control-height-md | 2.25rem | min-block-size |
--slean-space-3 | 0.75rem | padding-inline |
--slean-control-border | var(--slean-border-strong) | border |
--slean-radius-md | 0.625rem | border-radius |
--slean-control-bg | var(--slean-surface) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-control-border-hover | var(--slean-accent) | border-color |
--slean-control-border-focus | var(--slean-accent) | border-color |
--slean-control-ring | 0 0 0 3px color-mix(in oklch, var(--slean-accent) 22%, transparent) | box-shadow |
--slean-warning | oklch(76% 0.16 80) | border-color |
--slean-control-ring-warning | 0 0 0 3px color-mix(in oklch, var(--slean-warning) 28%, transparent) | box-shadow |
--slean-danger | oklch(55% 0.2 25) | border-color |
--slean-control-ring-danger | 0 0 0 3px color-mix(in oklch, var(--slean-danger) 22%, transparent) | box-shadow |
--slean-control-border-disabled | var(--slean-border) | border-color |
--slean-control-bg-disabled | var(--slean-muted) | background-color |
--slean-text-md | 1rem | font-size |
State selectors the stylesheet targets, all from the platform or ARIA: ::-webkit-calendar-picker-indicator, ::placeholder, :disabled, :focus-visible, :hover, :user-invalid, [aria-invalid="true"].
Variant attributes: data-status (warning, error).
Controlled integration
The options are markup, so the application changes them by rendering. Bind the value to read what was typed or chosen.
<script lang="ts">
let { cities }: { cities: string[] } = $props();
let city = $state('');
</script>
<input id="autocomplete-city" name="city" list="autocomplete-city-options" bind:value={city}
data-slean="autocomplete" />
<!-- New options are new <option> elements; the browser filters them as the user types. -->
<datalist id="autocomplete-city-options">
{#each cities as name (name)}
<option value={name}></option>
{/each}
</datalist>Compatibility notes
<input list> is Baseline 2019. <datalist> is not Baseline: how options are matched, whether an option’s label is shown and whether a list appears at all differ between browsers. Where no list appears, the field is a plain text input and the typed value is still submitted.
Testing
apps/playground/tests/primitives/inputs-overlays.spec.tsPlaywright, with page JavaScript enabled and disabled, and axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/autocomplete/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/autocomplete/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/autocomplete.cssthe optional stylesheet