Forms
Select
Chooses one option from a list. Two variants: the browser's own, and a custom list you can style.
Installation
npx sinaui add select custom-selectCopy the files listed in its registry entry,its registry entry, and install the packages it names.
Which one?
- Native for plain text options. Phones open their system picker, and it works with no JavaScript. Prefer it.
- Custom when options need a second line, an icon or a swatch, or the list must match your design exactly.
Native
Copy src/lib/ui/Select.svelte, Icon.svelte and tokens.css.
With a placeholder
The placeholder reads as a hint and can't be chosen back once something is picked.
<script lang="ts">
import Select from '#lib/ui/Select.svelte';
let city = $state('');
</script>
<Select label="Center of learning" placeholder="Choose a city" bind:value={city}>
<option value="baghdad">Baghdad</option>
<option value="cordoba">Córdoba</option>
<option value="cairo">Cairo</option>
<option value="samarkand">Samarkand</option>
</Select>Grouped options
Grouped by discipline.
<script lang="ts">
import Select from '#lib/ui/Select.svelte';
let work = $state('canon');
</script>
<Select label="Work" bind:value={work} hint="Grouped by discipline.">
<optgroup label="Medicine">
<option value="canon">The Canon of Medicine</option>
<option value="tasrif">Kitab al-Tasrif</option>
</optgroup>
<optgroup label="Mathematics">
<option value="jabr">Al-Jabr</option>
</optgroup>
<optgroup label="Optics">
<option value="manazir">Kitab al-Manazir</option>
</optgroup>
</Select>With an error
Choose where to read the manuscript.
<script lang="ts">
import Select from '#lib/ui/Select.svelte';
let room = $state('');
const error = $derived(room ? undefined : 'Choose where to read the manuscript.');
</script>
<Select label="Reading room" placeholder="Choose a room" bind:value={room} {error}>
<option value="baghdad">Bayt al-Hikma, Baghdad</option>
<option value="fez">Al-Qarawiyyin, Fez</option>
</Select>Custom
Copy src/lib/ui/CustomSelect.svelte, floating.ts, Icon.svelte and tokens.css. No other dependencies.
Basic
Type to jump: several letters in a row search together, so “sam” reaches Samarkand. With a name, a hidden input submits the value.
<script lang="ts">
import CustomSelect from '#lib/ui/CustomSelect.svelte';
let city = $state('');
const cities = [
{ value: 'baghdad', label: 'Baghdad' },
{ value: 'bukhara', label: 'Bukhara' },
{ value: 'cairo', label: 'Cairo' },
{ value: 'cordoba', label: 'Córdoba' },
{ value: 'damascus', label: 'Damascus' },
{ value: 'samarkand', label: 'Samarkand' }
];
</script>
<CustomSelect
label="City to visit"
placeholder="Choose a city"
options={cities}
bind:value={city}
name="city"
/>With descriptions
A second line per option. Disabled options stay visible but can't be chosen.
Nasta‘liq is being recut.
<script lang="ts">
import CustomSelect from '#lib/ui/CustomSelect.svelte';
let script = $state('naskh');
const scripts = [
{ value: 'kufic', label: 'Kufic', description: 'Angular; early Qur’an manuscripts.' },
{
value: 'naskh',
label: 'Naskh',
description: 'Rounded and legible; the scribe’s everyday hand.'
},
{
value: 'thuluth',
label: 'Thuluth',
description: 'Tall and sweeping; titles and inscriptions.'
},
{
value: 'nastaliq',
label: 'Nasta‘liq',
description: 'Flowing; Persian poetry.',
disabled: true
}
];
</script>
<CustomSelect
label="Script"
options={scripts}
bind:value={script}
hint="Nasta‘liq is being recut."
/>Custom rendering
The item snippet renders each option however you like; the check mark and keyboard behavior stay.
<script lang="ts">
import CustomSelect from '#lib/ui/CustomSelect.svelte';
let ink = $state('lapis');
const inks = [
{ value: 'lapis', label: 'Lapis blue', color: '#26619c' },
{ value: 'gall', label: 'Oak-gall black', color: '#1f1b16' },
{ value: 'vermilion', label: 'Vermilion', color: '#d0402b' },
{ value: 'gold', label: 'Gold leaf', color: '#c9a227' }
];
const colorOf = (v: string) => inks.find((i) => i.value === v)?.color;
</script>
<CustomSelect label="Ink" options={inks} bind:value={ink}>
{#snippet item(option)}
<span class="row">
<span class="swatch" style:background={colorOf(option.value)}></span>
{option.label}
</span>
{/snippet}
</CustomSelect>
<style>
.row {
display: flex;
align-items: center;
gap: 0.625rem;
}
.swatch {
inline-size: 0.875rem;
block-size: 0.875rem;
border-radius: 50%;
}
</style>Props
Select
| Prop | Type | Default |
|---|---|---|
label | string (required) | |
value | string, bindable | '' |
placeholder, hint, error | string | |
children | <option> and <optgroup> elements |
CustomSelect
| Prop | Type | Default |
|---|---|---|
label | string (required) | |
options | { value, label, description?, disabled? }[] | |
value | string, bindable | '' |
placeholder | string | 'Choose…' |
name | string: submits via a hidden input | |
hint, error | string | |
disabled | boolean | false |
item | Snippet (option, selected) | |
searchable | boolean: a search box above the list (example) | false |
searchLabel, empty | string | 'Search', 'No matches' |
Accessibility
- Native: a real
<select>with the platform's picker, keyboard and typeahead. - Custom: the WAI-ARIA select-only combobox pattern. Focus stays on the button (
role="combobox") and the highlighted option is announced througharia-activedescendant. - Both: the same label, hint and error wiring as Input; 16px text so iOS doesn't zoom.
Keyboard (custom)
| Key | Action |
|---|---|
| Enter / Space / ↓ / ↑ | Closed: opens at the current option. |
| ↓ / ↑ | Open: next / previous option (skips disabled). |
| Home / End, PageUp / PageDown | First / last, or ten at a time. |
| Letters | Jump to the first match; keep typing to refine. |
| Enter / Space / Tab | Chooses the highlighted option (Tab also moves on). |
| Escape | Closes without changing anything. |