Forms

Combobox

Chooses one option from a long list by typing part of its name.

Installation

npx sinaui add combobox

Examples

Basic

Matches anywhere in the name, ignoring case and accents: “cordoba” finds Córdoba. With a name, a hidden input submits the value.

<script lang="ts">
	import Combobox from '#lib/ui/Combobox.svelte';

	let city = $state('');
	const cities = [
		'Aleppo',
		'Baghdad',
		'Balkh',
		'Basra',
		'Bukhara',
		'Cairo',
		'Córdoba',
		'Damascus',
		'Fez',
		'Isfahan',
		'Kairouan',
		'Merv',
		'Nīshāpūr',
		'Palermo',
		'Rayy',
		'Samarkand',
		'Toledo'
	].map((label) => ({ value: label.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase(), label }));
</script>

<Combobox
	label="City of study"
	placeholder="Search cities"
	options={cities}
	bind:value={city}
	name="city"
/>

With descriptions

A second line per option. Disabled options still show up in results but can't be chosen.

Try “sina” or “haytham”: accents don't matter.

<script lang="ts">
	import Combobox from '#lib/ui/Combobox.svelte';

	let scholar = $state('ibn-sina');
	const scholars = [
		{ value: 'khwarizmi', label: 'Al-Khwārizmī', description: 'Algebra · Baghdad' },
		{ value: 'kindi', label: 'Al-Kindī', description: 'Philosophy · Baghdad' },
		{ value: 'razi', label: 'Al-Rāzī', description: 'Medicine · Rayy' },
		{ value: 'ibn-sina', label: 'Ibn Sīnā', description: 'Medicine · Bukhara' },
		{ value: 'haytham', label: 'Ibn al-Haytham', description: 'Optics · Cairo' },
		{ value: 'biruni', label: 'Al-Bīrūnī', description: 'Astronomy · Ghazni' },
		{ value: 'zahrawi', label: 'Al-Zahrāwī', description: 'Surgery · Córdoba' },
		{ value: 'khayyam', label: 'Omar Khayyām', description: 'Mathematics · Nīshāpūr' },
		{ value: 'idrisi', label: 'Al-Idrīsī', description: 'Geography · Palermo' },
		{
			value: 'ibn-battuta',
			label: 'Ibn Baṭṭūṭa',
			description: 'Away on a journey',
			disabled: true
		}
	];
</script>

<Combobox
	label="Scholar"
	options={scholars}
	bind:value={scholar}
	hint="Try “sina” or “haytham”: accents don't matter."
/>

Search inside the list

A button that opens a list with its own search box: the chosen value can't be typed over, only searched for. This is Select's searchable option.

<script lang="ts">
	import CustomSelect from '#lib/ui/CustomSelect.svelte';

	const scholars = [
		['Abbas ibn Firnas', 'Flight, Córdoba'],
		['Al-Battani', 'Astronomy, Raqqa'],
		['Al-Biruni', 'Astronomy, Ghazni'],
		['Al-Farabi', 'Philosophy, Baghdad'],
		['Al-Idrisi', 'Geography, Palermo'],
		['Al-Jazari', 'Engineering, Diyarbakır'],
		['Al-Khwarizmi', 'Mathematics, Baghdad'],
		['Al-Kindi', 'Philosophy, Baghdad'],
		['Al-Razi', 'Medicine, Rayy'],
		['Al-Zahrawi', 'Surgery, Córdoba'],
		['Banu Musa', 'Mechanics, Baghdad'],
		['Fatima al-Fihri', 'Founded al-Qarawiyyin, Fez'],
		['Ibn al-Haytham', 'Optics, Cairo'],
		['Ibn Battuta', 'Travel, Tangier'],
		['Ibn Khaldun', 'History, Tunis'],
		['Ibn Rushd', 'Philosophy, Córdoba'],
		['Ibn Sina', 'Medicine, Bukhara'],
		['Maryam al-Ijliya', 'Astrolabes, Aleppo'],
		['Nasir al-Din al-Tusi', 'Astronomy, Maragheh'],
		['Omar Khayyam', 'Mathematics, Nishapur'],
		['Sutayta al-Mahamali', 'Mathematics, Baghdad'],
		['Thabit ibn Qurra', 'Mathematics, Baghdad']
	].map(([label, description]) => ({
		value: label.toLowerCase().replace(/\W+/g, '-'),
		label,
		description
	}));
	let mentor = $state('');
</script>

<CustomSelect
	label="Mentor"
	options={scholars}
	bind:value={mentor}
	placeholder="Choose a scholar"
	searchable
	searchLabel="Search scholars"
	empty="No scholar by that name"
/>

Thousands of options

With TanStack Query: the search goes to the server as you type, and the next page loads as you scroll near the end.

10,000 entries, loaded 40 at a time as you scroll.

<script lang="ts">
	import { QueryClient, createInfiniteQuery } from '@tanstack/svelte-query';
	import Combobox from '#lib/ui/Combobox.svelte';

	// Stands in for your API: 10,000 catalog entries, searched on the "server", 40 to a page.
	const subjects = [
		'Optics',
		'Algebra',
		'Medicine',
		'Astronomy',
		'Geography',
		'Poetry',
		'Logic',
		'Music'
	];
	const cities = [
		'Baghdad',
		'Cairo',
		'Córdoba',
		'Fez',
		'Damascus',
		'Bukhara',
		'Samarkand',
		'Isfahan'
	];
	const catalog = Array.from({ length: 10_000 }, (_, i) => ({
		value: `ms-${i + 1}`,
		label: `${subjects[i % 8]}, ${cities[Math.floor(i / 8) % 8]} copy ${Math.floor(i / 64) + 1}`,
		description: `Shelf ${(i % 400) + 1}`
	}));
	const fold = (s: string) => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
	async function fetchPage(search: string, page: number) {
		await new Promise((r) => setTimeout(r, 350));
		const q = fold(search.trim());
		const hits = q ? catalog.filter((m) => fold(m.label).includes(q)) : catalog;
		const next = (page + 1) * 40 < hits.length ? page + 1 : undefined;
		return { items: hits.slice(page * 40, page * 40 + 40), next };
	}

	const client = new QueryClient();
	let search = $state('');
	let timer: ReturnType<typeof setTimeout>;
	// A short pause after typing, so a word is one request rather than one per letter.
	const onsearch = (q: string) => {
		clearTimeout(timer);
		timer = setTimeout(() => (search = q), 200);
	};

	const query = createInfiniteQuery(
		() => ({
			queryKey: ['catalog', search],
			queryFn: ({ pageParam }) => fetchPage(search, pageParam),
			initialPageParam: 0,
			getNextPageParam: (last) => last.next
		}),
		() => client
	);
	const options = $derived(query.data?.pages.flatMap((p) => p.items) ?? []);
	let manuscript = $state('');
</script>

<Combobox
	label="Manuscript"
	placeholder="Search the catalog"
	hint="10,000 entries, loaded 40 at a time as you scroll."
	{options}
	bind:value={manuscript}
	{onsearch}
	onloadmore={() => query.hasNextPage && !query.isFetchingNextPage && query.fetchNextPage()}
	loading={query.isFetching}
/>

With an error

Choose where to collect the manuscript.

<script lang="ts">
	import Combobox from '#lib/ui/Combobox.svelte';

	let room = $state('');
	const rooms = [
		{ value: 'bayt', label: 'Bayt al-Ḥikma, Baghdad' },
		{ value: 'qarawiyyin', label: 'Al-Qarawiyyīn, Fez' },
		{ value: 'azhar', label: 'Al-Azhar, Cairo' }
	];
</script>

<Combobox
	label="Reading room"
	options={rooms}
	bind:value={room}
	error={room ? undefined : 'Choose where to collect the manuscript.'}
/>

Props

PropTypeDefault
labelstring (required)
options{ value, label, description?, disabled? }[]
valuestring, bindable''
placeholderstring
hintstring
errorstring
namestring
emptystring'No matches'
disabledbooleanfalse
onsearch(query) => void: you filter; options show as given
onloadmore() => void: scrolled near the end
loadingbooleanfalse

Accessibility

  • Follows the WAI-ARIA combobox pattern with list autocomplete: focus stays in the text box and points at the highlighted option.
  • Screen readers hear how many options match as you type.
  • Typing never changes the value on its own. Leaving the field without choosing puts back the last choice.
KeyAction
↓ / ↑Opens the list, then moves through it.
EnterChooses the highlighted option.
TabChooses the highlighted option and moves on.
EscCloses the list and undoes the typing. Pressed again, clears the field.
Alt + ↓Opens the list without moving.
Introduction
Theming
Changelog
Button
Action button
Confirm button
Copy button
Hold to confirm
Split button
Calendar
Checkbox
Color picker
Combobox
Date picker
Date range picker
File drop
Form
Input
Mention input
Multi select
Number field
OTP input
Password input
Phone input
Radio cards
Radio group
Rich text editor
Search field
Segmented control
Select
Signature pad
Slider
Switch
Tag input
Upload
Wheel picker
Textarea
Time field
Toggle group
Toolbar
Alert dialog
Command palette
Context menu
Dialog
Drawer
Dropdown
Hover card
Popover
Tooltip
Announcement bar
Alert
Empty state
Meter
Progress
Rolling number
Skeleton
Spinner
Toast
Avatar
Avatar group
Badge
Card
Carousel
Chart
Chat
Code block
Data table
Folder
Kbd
Mind map
Resizable panels
Scroll area
Separator
Sortable
Table
Tag group
Virtual list
Audio player
Image
Image compare
Video
Visualizer
Breadcrumb
File tree
Menubar
Navigation menu
Pagination
Sidebar layout
Stepper
Tabs
Alarm clock
Clock
Dynamic island
Fluid orb
Hourglass
Stopwatch
Timer
AI chat
AI media
Auth
Board
Booking
Dashboard
File explorer
Messaging
Notifications
Settings
Team
Overview
Pricing
Agent chat
Calendar
Voice agent
Mail inbox
CMS
Blog
Marketing site
E-commerce
AI agents
SaaS landing page
SaaS app
Accordion
Dark theme
Light theme