Combobox
Enables users to pick from a list of options displayed in a dropdown.
	<script lang="ts">
  import { Combobox } from "bits-ui";
  import CaretUpDown from "phosphor-svelte/lib/CaretUpDown";
  import Check from "phosphor-svelte/lib/Check";
  import OrangeSlice from "phosphor-svelte/lib/OrangeSlice";
 
  const fruits = [
    { value: "mango", label: "Mango" },
    { value: "watermelon", label: "Watermelon" },
    { value: "apple", label: "Apple" },
    { value: "pineapple", label: "Pineapple" },
    { value: "orange", label: "Orange" },
    { value: "grape", label: "Grape" },
    { value: "strawberry", label: "Strawberry" },
    { value: "banana", label: "Banana" },
    { value: "kiwi", label: "Kiwi" },
    { value: "peach", label: "Peach" },
    { value: "cherry", label: "Cherry" },
    { value: "blueberry", label: "Blueberry" },
    { value: "raspberry", label: "Raspberry" },
    { value: "blackberry", label: "Blackberry" },
    { value: "plum", label: "Plum" },
    { value: "apricot", label: "Apricot" },
    { value: "pear", label: "Pear" },
    { value: "grapefruit", label: "Grapefruit" }
  ];
 
  let searchValue = $state("");
 
  const filteredFruits = $derived(
    searchValue === ""
      ? fruits
      : fruits.filter((fruit) =>
          fruit.label.toLowerCase().includes(searchValue.toLowerCase())
        )
  );
</script>
 
<Combobox.Root
  type="single"
  name="favoriteFruit"
  onOpenChange={(o) => {
    if (!o) searchValue = "";
  }}
>
  <div class="relative">
    <OrangeSlice
      class="absolute start-3 top-1/2 size-6 -translate-y-1/2 text-muted-foreground"
    />
    <Combobox.Input
      oninput={(e) => (searchValue = e.currentTarget.value)}
      class="inline-flex h-input w-[296px] truncate rounded-9px border border-border-input bg-background px-11 text-sm transition-colors placeholder:text-foreground-alt/50 focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2 focus:ring-offset-background"
      placeholder="Search a fruit"
      aria-label="Search a fruit"
    />
    <Combobox.Trigger class="absolute end-3 top-1/2 size-6 -translate-y-1/2">
      <CaretUpDown class="size-6 text-muted-foreground" />
    </Combobox.Trigger>
  </div>
  <Combobox.Portal>
    <Combobox.Content
      class="max-h-96 w-[var(--bits-combobox-trigger-width)] min-w-[var(--bits-combobox-trigger-width)] overflow-y-auto rounded-xl border border-muted bg-background px-1 py-3 shadow-popover outline-none"
      sideOffset={10}
    >
      {#each filteredFruits as fruit, i (i + fruit.value)}
        <Combobox.Item
          class="flex h-10 w-full select-none items-center rounded-button py-3 pl-5 pr-1.5 text-sm capitalize outline-none duration-75 data-[highlighted]:bg-muted"
          value={fruit.value}
          label={fruit.label}
        >
          {#snippet children({ selected })}
            {fruit.label}
            {#if selected}
              <div class="ml-auto">
                <Check />
              </div>
            {/if}
          {/snippet}
        </Combobox.Item>
      {:else}
        <span class="block px-5 py-2 text-sm text-muted-foreground">
          No results found
        </span>
      {/each}
    </Combobox.Content>
  </Combobox.Portal>
</Combobox.Root>
	import typography from "@tailwindcss/typography";
	import animate from "tailwindcss-animate";
	import { fontFamily } from "tailwindcss/defaultTheme";
	 
	/** @type {import('tailwindcss').Config} */
	export default {
		darkMode: "class",
		content: ["./src/**/*.{html,js,svelte,ts}"],
		theme: {
			container: {
				center: true,
				screens: {
					"2xl": "1440px",
				},
			},
			extend: {
				colors: {
					border: {
						DEFAULT: "hsl(var(--border-card))",
						input: "hsl(var(--border-input))",
						"input-hover": "hsl(var(--border-input-hover))",
					},
					background: {
						DEFAULT: "hsl(var(--background) / <alpha-value>)",
						alt: "hsl(var(--background-alt) / <alpha-value>)",
					},
					foreground: {
						DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
						alt: "hsl(var(--foreground-alt) / <alpha-value>)",
					},
					muted: {
						DEFAULT: "hsl(var(--muted) / <alpha-value>)",
						foreground: "hsl(var(--muted-foreground))",
					},
					dark: {
						DEFAULT: "hsl(var(--dark) / <alpha-value>)",
						4: "hsl(var(--dark-04))",
						10: "hsl(var(--dark-10))",
						40: "hsl(var(--dark-40))",
					},
					accent: {
						DEFAULT: "hsl(var(--accent) / <alpha-value>)",
						foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
					},
					destructive: {
						DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
					},
					contrast: {
						DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
					},
				},
				fontFamily: {
					sans: ["Inter", ...fontFamily.sans],
					mono: ["Source Code Pro", ...fontFamily.mono],
					alt: ["Courier", ...fontFamily.sans],
				},
				fontSize: {
					xxs: "10px",
				},
				borderWidth: {
					6: "6px",
				},
				borderRadius: {
					card: "16px",
					"card-lg": "20px",
					"card-sm": "10px",
					input: "9px",
					button: "5px",
					"5px": "5px",
					"9px": "9px",
					"10px": "10px",
					"15px": "15px",
				},
				height: {
					input: "3rem",
					"input-sm": "2.5rem",
				},
				boxShadow: {
					mini: "var(--shadow-mini)",
					"mini-inset": "var(--shadow-mini-inset)",
					popover: "var(--shadow-popover)",
					kbd: "var(--shadow-kbd)",
					btn: "var(--shadow-btn)",
					card: "var(--shadow-card)",
					"date-field-focus": "var(--shadow-date-field-focus)",
				},
				opacity: {
					8: "0.08",
				},
				scale: {
					80: ".80",
					98: ".98",
					99: ".99",
				},
			},
			keyframes: {
				"accordion-down": {
					from: { height: "0" },
					to: { height: "var(--bits-accordion-content-height)" },
				},
				"accordion-up": {
					from: { height: "var(--bits-accordion-content-height)" },
					to: { height: "0" },
				},
				"caret-blink": {
					"0%,70%,100%": { opacity: "1" },
					"20%,50%": { opacity: "0" },
				},
			},
			animation: {
				"accordion-down": "accordion-down 0.2s ease-out",
				"accordion-up": "accordion-up 0.2s ease-out",
				"caret-blink": "caret-blink 1.25s ease-out infinite",
			},
		},
		plugins: [typography, animate],
	};
		@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
 
@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer base {
	:root {
		/* Colors */
		--background: 0 0% 100%;
		--background-alt: 0 0% 100%;
		--foreground: 0 0% 9%;
		--foreground-alt: 0 0% 32%;
		--muted: 240 5% 96%;
		--muted-foreground: 0 0% 9% / 0.4;
		--border: 240 6% 10%;
		--border-input: 240 6% 10% / 0.17;
		--border-input-hover: 240 6% 10% / 0.4;
		--border-card: 240 6% 10% / 0.1;
		--dark: 240 6% 10%;
		--dark-10: 240 6% 10% / 0.1;
		--dark-40: 240 6% 10% / 0.4;
		--dark-04: 240 6% 10% / 0.04;
		--accent: 204 94% 94%;
		--accent-foreground: 204 80% 16%;
		--destructive: 347 77% 50%;
 
		/* black */
		--constrast: 0 0% 0%;
 
		/* Shadows */
		--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
		--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
		--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
		--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
		--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
		--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
		--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
	}
 
	.dark {
		/* Colors */
		--background: 0 0% 5%;
		--background-alt: 0 0% 8%;
		--foreground: 0 0% 95%;
		--foreground-alt: 0 0% 70%;
		--muted: 240 4% 16%;
		--muted-foreground: 0 0% 100% / 0.4;
		--border: 0 0% 96%;
		--border-input: 0 0% 96% / 0.17;
		--border-input-hover: 0 0% 96% / 0.4;
		--border-card: 0 0% 96% / 0.1;
		--dark: 0 0% 96%;
		--dark-40: 0 0% 96% / 0.4;
		--dark-10: 0 0% 96% / 0.1;
		--dark-04: 0 0% 96% / 0.04;
		--accent: 204 90 90%;
		--accent-foreground: 204 94% 94%;
		--destructive: 350 89% 60%;
 
		/* white */
		--constrast: 0 0% 100%;
 
		/* Shadows */
		--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
		--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
		--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
		--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
		--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
		--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
		--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
	}
}
 
@layer base {
	* {
		@apply border-border;
	}
	html {
		-webkit-text-size-adjust: 100%;
		font-variation-settings: normal;
	}
	body {
		@apply bg-background text-foreground;
		font-feature-settings:
			"rlig" 1,
			"calt" 1;
	}
 
	/* Mobile tap highlight */
	/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
	html {
		-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
	}
	::selection {
		background: #fdffa4;
		color: black;
	}
 
	/* === Scrollbars === */
 
	::-webkit-scrollbar {
		@apply w-2;
		@apply h-2;
	}
 
	::-webkit-scrollbar-track {
		@apply !bg-transparent;
	}
	::-webkit-scrollbar-thumb {
		@apply rounded-card-lg !bg-dark-10;
	}
 
	::-webkit-scrollbar-corner {
		background: rgba(0, 0, 0, 0);
	}
 
	/* Firefox */
	/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
 
	html {
		scrollbar-color: var(--bg-muted);
	}
 
	.antialised {
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
	}
}
 
@layer utilities {
	.step {
		counter-increment: step;
	}
 
	.step:before {
		@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
		@apply ml-[-50px] mt-[-4px];
		content: counter(step);
	}
}
 
@layer components {
	*:not(body):not(.focus-override) {
		outline: none !important;
		&:focus-visible {
			@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
		}
	}
 
	.link {
		@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
	}
 
	input::-webkit-outer-spin-button,
	input::-webkit-inner-spin-button {
		-webkit-appearance: none;
		margin: 0;
	}
 
	/* Firefox */
	input[type="number"] {
		-moz-appearance: textfield;
	}
}
Structure
	<script lang="ts">
	import { Combobox } from "bits-ui";
</script>
 
<Combobox.Root>
	<Combobox.Input />
	<Combobox.Trigger />
	<Combobox.Portal>
		<Combobox.Content>
			<Combobox.Group>
				<Combobox.GroupLabel />
				<Combobox.Item />
			</Combobox.Group>
			<Combobox.Item />
		</Combobox.Content>
	</Combobox.Portal>
</Combobox.Root>
Reusable Components
It's recommended to use the Combobox primitives to build your own custom combobox component that can be reused throughout your application.
	<script lang="ts">
	import { Combobox, type WithoutChildrenOrChild, mergeProps } from "bits-ui";
 
	type Item = { value: string; label: string; };
 
	type Props = Combobox.RootProps & {
		items: Item[];
		inputProps?: WithoutChildrenOrChild<Combobox.InputProps>;
		contentProps?: WithoutChildrenOrChild<Combobox.ContentProps>;
	}
 
	let {
		items,
		value = $bindable(),
		open = $bindable(false),
		inputProps,
		contentProps,
		...restProps
	}: Props = $props();
 
	let searchValue = $state("");
 
	const filteredItems = $derived.by(() => {
		if (searchValue === "") return items;
		return items.filter((item) => item.label.toLowerCase().includes(searchValue.toLowerCase()));
	})
 
	function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
		searchValue = e.currentTarget.value;
	}
 
	function handleOpenChange(newOpen: boolean) {
		if (!newOpen) searchValue = "";
	}
 
	const mergedRootProps = $derived(mergeProps(restProps, { onOpenChange: handleOpenChange }))
	const mergedInputProps = $derived(mergeProps(inputProps, { oninput: handleInput } ))
</script>
 
<Combobox.Root bind:value bind:open {...mergedRootProps}>
	<Combobox.Input {....mergedInputProps} />
	<Combobox.Trigger>Open</Combobox.Trigger>
	<Combobox.Portal>
		<Combobox.Content {...contentProps}>
			{#each filteredItems as item, i (i + item.value)}
				<Combobox.Item value={item.value} label={item.label}>
					{#snippet children({ selected })}
						{item.label}
						{selected ? "✅" : ""}
					{/snippet}
				</Combobox.Item>
			{:else}
				<span>
					No results found
				</span>
			{/each}
		</Combobox.Content>
	</Combobox.Portal>
</Combobox.Root>
	<script lang="ts">
	import { CustomCombobox } from "$lib/components";
 
	const items = [
		{ value: "mango", label: "Mango" },
		{ value: "watermelon", label: "Watermelon" },
		{ value: "apple", label: "Apple" },
		// ...
	];
</script>
 
<CustomCombobox {items} />
API Reference
The root combobox component which manages & scopes the state of the combobox.
| Property | Type | Description | 
|---|---|---|
type  Required  |  enum   |  The type of combobox. Default:  undefined | 
value    bindable prop |  union   |  The value of the combobox. When the type is  Default:  undefined | 
onValueChange   |  function   |  A callback that is fired when the combobox value changes. When the type is  Default:  undefined | 
open    bindable prop |  boolean |  The open state of the combobox menu. Default:  false | 
onOpenChange   |  function   |  A callback that is fired when the combobox menu's open state changes. Default:  undefined | 
disabled   |  boolean |  Whether or not the combobox component is disabled. Default:  false | 
name   |  string |  The name to apply to the hidden input element for form submission. If provided, a hidden input element will be rendered to submit the value of the combobox. Default:  undefined | 
required   |  boolean |  Whether or not the combobox menu is required. Default:  false | 
scrollAlignment   |  enum   |  The alignment of the highlighted item when scrolling. Default:  'nearest' | 
loop   |  boolean |  Whether or not the combobox menu should loop through items. Default:  false | 
children   |  Snippet |  The children content to render. Default:  undefined | 
The element which contains the combobox's items.
| Property | Type | Description | 
|---|---|---|
side   |  enum   |  The preferred side of the anchor to render the floating element against when open. Will be reversed when collisions occur. Default:  bottom | 
sideOffset   |  number |  The distance in pixels from the anchor to the floating element. Default:  0 | 
align   |  enum   |  The preferred alignment of the anchor to render the floating element against when open. This may change when collisions occur. Default:  start | 
alignOffset   |  number |  The distance in pixels from the anchor to the floating element. Default:  0 | 
arrowPadding   |  number |  The amount in pixels of virtual padding around the viewport edges to check for overflow which will cause a collision. Default:  0 | 
avoidCollisions   |  boolean |  When  Default:  true | 
collisionBoundary   |  union   |  A boundary element or array of elements to check for collisions against. Default:  undefined | 
collisionPadding   |  union   |  The amount in pixels of virtual padding around the viewport edges to check for overflow which will cause a collision. Default:  0 | 
sticky   |  enum   |  The sticky behavior on the align axis.  Default:  partial | 
hideWhenDetached   |  boolean |  When  Default:  true | 
updatePositionStrategy   |  enum   |  The strategy to use when updating the position of the content. When  Default:  optimized | 
strategy   |  enum   |  The positioning strategy to use for the floating element. When  Default:  fixed | 
preventScroll   |  boolean |  When  Default:  true | 
onEscapeKeydown   |  function   |  Callback fired when an escape keydown event occurs in the floating content. You can call  Default:  undefined | 
escapeKeydownBehavior   |  enum   |  The behavior to use when an escape keydown event occurs in the floating content.  Default:  close | 
onInteractOutside   |  function   |  Callback fired when an outside interaction event completes, which is either a  Default:  undefined | 
onInteractOutsideStart   |  function   |  Callback fired when an outside interaction event starts, which is either a  Default:  undefined | 
onFocusOutside   |  function   |  Callback fired when focus leaves the dismissable layer. You can call  Default:  undefined | 
interactOutsideBehavior   |  enum   |  The behavior to use when an interaction occurs outside of the floating content.  Default:  close | 
onMountAutoFocus   |  function   |  Event handler called when auto-focusing the content as it is mounted. Can be prevented. Default:  undefined | 
onDestroyAutoFocus   |  function   |  Event handler called when auto-focusing the content as it is destroyed. Can be prevented. Default:  undefined | 
trapFocus   |  boolean |  Whether or not to trap the focus within the content when open. Default:  true | 
preventOverflowTextSelection   |  boolean |  When  Default:  true | 
dir   |  enum   |  The reading direction of the app. Default:  ltr | 
loop   |  boolean |  Whether or not the combobox should loop through items when reaching the end. Default:  false | 
forceMount   |  boolean |  Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default:  false | 
ref    bindable prop |  HTMLDivElement |  The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default:  undefined | 
children   |  Snippet |  The children content to render. Default:  undefined | 
child   |  Snippet   |  Use render delegation to render your own element. See delegation docs for more information. Default:  undefined | 
| Data Attribute | Value | Description | 
|---|---|---|
data-state |  enum   |  The combobox's open state.  | 
data-combobox-content |  '' |  Present on the content element.  | 
| CSS Variable | Description | 
|---|---|
--bits-combobox-content-transform-origin |  The transform origin of the combobox content element.  | 
--bits-combobox-content-available-width |  The available width of the combobox content element.  | 
--bits-combobox-content-available-height |  The available height of the combobox content element.  | 
--bits-combobox-trigger-width |  The width of the combobox trigger element.  | 
--bits-combobox-trigger-height |  The height of the combobox trigger element.  | 
A combobox item, which must be a child of the Combobox.Content component.
| Property | Type | Description | 
|---|---|---|
value  Required  |  string |  The value of the item. Default:  undefined | 
label   |  string |  The label of the item, which is what the list will be filtered by. Default:  undefined | 
disabled   |  boolean |  Whether or not the combobox item is disabled. This will prevent interaction/selection. Default:  false | 
onHighlight   |  function   |  A callback that is fired when the item is highlighted. Default:  undefined | 
onUnhighlight   |  function   |  A callback that is fired when the item is unhighlighted. Default:  undefined | 
ref    bindable prop |  HTMLDivElement |  The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default:  undefined | 
children   |  Snippet |  The children content to render. Default:  undefined | 
child   |  Snippet   |  Use render delegation to render your own element. See delegation docs for more information. Default:  undefined | 
| Data Attribute | Value | Description | 
|---|---|---|
data-value |  '' |  The value of the combobox item.  | 
data-label |  '' |  The label of the combobox item.  | 
data-disabled |  '' |  Present when the item is disabled.  | 
data-highlighted |  '' |  Present when the item is highlighted, which is either via keyboard navigation of the menu or hover.  | 
data-selected |  '' |  Present when the item is selected.  | 
data-combobox-item |  '' |  Present on the item element.  | 
A representation of the combobox input element, which is typically displayed in the content.
| Property | Type | Description | 
|---|---|---|
defaultValue   |  string |  The default value of the input. This is not a reactive prop and is only used to populate the input when the combobox is first mounted if there is already a value set. Default:  undefined | 
ref    bindable prop |  HTMLInputElement |  The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default:  undefined | 
children   |  Snippet |  The children content to render. Default:  undefined | 
child   |  Snippet   |  Use render delegation to render your own element. See delegation docs for more information. Default:  undefined | 
| Data Attribute | Value | Description | 
|---|---|---|
data-state |  enum   |  The combobox's open state.  | 
data-disabled |  '' |  Present when the combobox is disabled.  | 
data-combobox-input |  '' |  Present on the input element.  | 
A label for the parent combobox group. This is used to describe a group of related combobox items.
| Property | Type | Description | 
|---|---|---|
ref    bindable prop |  HTMLDivElement |  The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default:  undefined | 
children   |  Snippet |  The children content to render. Default:  undefined | 
child   |  Snippet   |  Use render delegation to render your own element. See delegation docs for more information. Default:  undefined | 
| Data Attribute | Value | Description | 
|---|---|---|
data-combobox-group-label |  '' |  Present on the group label element.  | 
An optional arrow element which points to the content when open.
| Property | Type | Description | 
|---|---|---|
width   |  number |  The width of the arrow in pixels. Default:  8 | 
height   |  number |  The height of the arrow in pixels. Default:  8 | 
ref    bindable prop |  HTMLDivElement |  The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default:  undefined | 
children   |  Snippet |  The children content to render. Default:  undefined | 
child   |  Snippet   |  Use render delegation to render your own element. See delegation docs for more information. Default:  undefined | 
| Data Attribute | Value | Description | 
|---|---|---|
data-arrow |  '' |  Present on the arrow element.  |