sveltelean Primitives
Versionv0.2.0 GitHub

Example

Team
<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>
Tier 0: the Svelte source adds the stylesheet import and a binding. Type a letter or press ArrowDown for the browser’s list; any other text is kept. Everything here works with page JavaScript disabled.

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/styles
stylesheets
import '@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

PartElementdata-slean-partRequiredNotes
root<input list="<datalist id>" data-slean="autocomplete">–yesA text, search, email, url or tel input with a label and a name.
options<datalist id="…"> with <option value="…">–yesReferenced 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 list attribute to the combobox role; no ARIA is added.
  • A label through <label for>; a hint through aria-describedby.
  • The list’s keyboard (ArrowDown, ArrowUp, Enter, Escape) is the browser’s and differs slightly between browsers.

Keyboard

KeyWhenResult
charactersfocus in the fieldTypes and filters the list (native; matching differs between browsers)
ArrowDown/ArrowUpfocus in the fieldOpens the list and moves through the options (native)
Enteran option highlightedWrites the option’s value into the field (native)
Escapelist openCloses the list and keeps the text (native)

Platform features

FeatureBaselineOutside the target
<input list>Baseline 2019A plain text input
<datalist>Not Baseline: matching, labels and whether a list appears differ by browserA 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:

TokenDefault (light)Applies to
--slean-control-height-md2.25remmin-block-size
--slean-space-30.75rempadding-inline
--slean-control-bordervar(--slean-border-strong)border
--slean-radius-md0.625remborder-radius
--slean-control-bgvar(--slean-surface)background
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-control-border-hovervar(--slean-accent)border-color
--slean-control-border-focusvar(--slean-accent)border-color
--slean-control-ring0 0 0 3px color-mix(in oklch, var(--slean-accent) 22%, transparent)box-shadow
--slean-warningoklch(76% 0.16 80)border-color
--slean-control-ring-warning0 0 0 3px color-mix(in oklch, var(--slean-warning) 28%, transparent)box-shadow
--slean-dangeroklch(55% 0.2 25)border-color
--slean-control-ring-danger0 0 0 3px color-mix(in oklch, var(--slean-danger) 22%, transparent)box-shadow
--slean-control-border-disabledvar(--slean-border)border-color
--slean-control-bg-disabledvar(--slean-muted)background-color
--slean-text-md1remfont-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.

city.svelte
<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

Source