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";
import CaretDoubleUp from "phosphor-svelte/lib/CaretDoubleUp";
import CaretDoubleDown from "phosphor-svelte/lib/CaretDoubleDown";
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="multiple"
name="favoriteFruit"
onOpenChange={(o) => {
if (!o) searchValue = "";
}}
>
<div class="relative">
<OrangeSlice
class="text-muted-foreground absolute start-3 top-1/2 size-6 -translate-y-1/2"
/>
<Combobox.Input
oninput={(e) => (searchValue = e.currentTarget.value)}
class="h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] touch-none truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm"
placeholder="Search a fruit"
aria-label="Search a fruit"
/>
<Combobox.Trigger
class="absolute end-3 top-1/2 size-6 -translate-y-1/2 touch-none"
>
<CaretUpDown class="text-muted-foreground size-6" />
</Combobox.Trigger>
</div>
<Combobox.Portal>
<Combobox.Content
class="focus-override border-muted bg-background shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden z-50 h-96 max-h-[var(--bits-combobox-content-available-height)] w-[var(--bits-combobox-anchor-width)] min-w-[var(--bits-combobox-anchor-width)] select-none rounded-xl border px-1 py-3 data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
sideOffset={10}
>
<Combobox.ScrollUpButton
class="flex w-full items-center justify-center py-1"
>
<CaretDoubleUp class="size-3" />
</Combobox.ScrollUpButton>
<Combobox.Viewport class="p-1">
{#each filteredFruits as fruit, i (i + fruit.value)}
<Combobox.Item
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 w-full select-none items-center py-3 pl-5 pr-1.5 text-sm capitalize"
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, try again.
</span>
{/each}
</Combobox.Viewport>
<Combobox.ScrollDownButton
class="flex w-full items-center justify-center py-1"
>
<CaretDoubleDown class="size-3" />
</Combobox.ScrollDownButton>
</Combobox.Content>
</Combobox.Portal>
</Combobox.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Overview
The Combobox component combines the functionality of an input field with a dropdown list of selectable options. It provides users with the ability to search, filter, and select from a predefined set of choices.
Key Features
- Keyboard Navigation: Full support for keyboard interactions, allowing users to navigate and select options without using a mouse.
- Customizable Rendering: Flexible architecture for rendering options, including support for grouped items.
- Accessibility: Built with ARIA attributes and keyboard interactions to ensure screen reader compatibility and accessibility standards.
- Portal Support: Ability to render the dropdown content in a portal, preventing layout issues in complex UI structures.
Architecture
The Combobox component is composed of several sub-components, each with a specific role:
- Root: The main container component that manages the state and context for the combobox.
- Input: The input field that allows users to enter search queries.
- Trigger: The button or element that opens the dropdown list.
- Portal: Responsible for portalling the dropdown content to the body or a custom target.
- Group: A container for grouped items, used to group related items.
- GroupHeading: A heading for a group of items, providing a descriptive label for the group.
- Item: An individual item within the list.
- Separator: A visual separator between items.
- Content: The dropdown container that displays the items. It uses Floating UI to position the content relative to the trigger.
- ContentStatic: An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself.
- Viewport: The visible area of the dropdown content, used to determine the size and scroll behavior.
- ScrollUpButton: A button that scrolls the content up when the content is larger than the viewport.
- ScrollDownButton: A button that scrolls the content down when the content is larger than the viewport.
- Arrow: An arrow element that points to the trigger when using the
Combobox.Content
component.
Structure
Here's an overview of how the Combobox component is structured in code:
<script lang="ts">
import { Combobox } from "bits-ui";
</script>
<Combobox.Root>
<Combobox.Input />
<Combobox.Trigger />
<Combobox.Portal>
<Combobox.Content>
<Combobox.Group>
<Combobox.GroupHeading />
<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 Props = Combobox.RootProps & {
inputProps?: WithoutChildrenOrChild<Combobox.InputProps>;
contentProps?: WithoutChildrenOrChild<Combobox.ContentProps>;
};
let {
items = [],
value = $bindable(),
open = $bindable(false),
inputProps,
contentProps,
type,
...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>
<!--
Destructuring (required for bindable) and discriminated unions don't play well together,
so we cast the value to `never` to avoid type errors here. However, on the consumer
side, the component will still be type-checked correctly.
-->
<Combobox.Root
{type}
{items}
bind:value={value as never}
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 {...item}>
{#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 type="single" {items} />
Managing Value State
This section covers how to manage the value
state of the Combobox.
Two-Way Binding
Use bind:value
for simple, automatic state synchronization:
<script lang="ts">
import { Combobox } from "bits-ui";
let myValue = $state("");
</script>
<button onclick={() => (myValue = "A")}> Select A </button>
<Combobox.Root type="single" bind:value={myValue}>
<!-- ... -->
</Combobox.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { Combobox } from "bits-ui";
let myValue = $state("");
function getValue() {
return myValue;
}
function setValue(newValue: string) {
myValue = newValue;
}
</script>
<Combobox.Root type="single" bind:value={getValue, setValue}>
<!-- ... -->
</Combobox.Root>
Managing Open State
This section covers how to manage the open
state of the Combobox.
Two-Way Binding
Use bind:open
for simple, automatic state synchronization:
<script lang="ts">
import { Combobox } from "bits-ui";
let myOpen = $state(false);
</script>
<button onclick={() => (myOpen = true)}> Open </button>
<Combobox.Root bind:open={myOpen}>
<!-- ... -->
</Combobox.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { Combobox } from "bits-ui";
let myOpen = $state(false);
function getOpen() {
return myOpen;
}
function setOpen(newOpen: boolean) {
myOpen = newOpen;
}
</script>
<Combobox.Root type="single" bind:open={getOpen, setOpen}>
<!-- ... -->
</Combobox.Root>
Opt-out of Floating UI
When you use the Combobox.Content
component, Bits UI uses Floating UI to position the content relative to the trigger, similar to other popover-like components.
You can opt-out of this behavior by instead using the Combobox.ContentStatic
component.
<Combobox.Root>
<Combobox.Trigger />
<Combobox.Input />
<Combobox.Portal>
<Combobox.ContentStatic>
<Combobox.ScrollUpButton />
<Combobox.Viewport>
<Combobox.Item />
<Combobox.Group>
<Combobox.GroupHeading />
<Combobox.Item />
</Combobox.Group>
</Combobox.Viewport>
<Combobox.ScrollDownButton />
</Combobox.ContentStatic>
</Combobox.Portal>
</Combobox.Root>
When using this component, you'll need to handle the positioning of the content yourself. Keep in mind that using Combobox.Portal
alongside Combobox.ContentStatic
may result in some unexpected positioning behavior, feel free to not use the portal or work around it.
Custom Anchor
By default, the Combobox.Content
is anchored to the Combobox.Input
component, which determines where the content is positioned.
If you wish to instead anchor the content to a different element, you can pass either a selector string or an HTMLElement
to the customAnchor
prop of the Combobox.Content
component.
<script lang="ts">
import { Combobox } from "bits-ui";
let customAnchor = $state<HTMLElement>(null!);
</script>
<div bind:this={customAnchor}></div>
<Combobox.Root>
<Combobox.Trigger />
<Combobox.Input />
<Combobox.Content {customAnchor}>
<!-- ... -->
</Combobox.Content>
</Combobox.Root>
What is the Viewport?
The Combobox.Viewport
component is used to determine the size of the content in order to determine whether or not the scroll up and down buttons should be rendered.
If you wish to set a minimum/maximum height for the select content, you should apply it to the Combobox.Viewport
component.
Scroll Up/Down Buttons
The Combobox.ScrollUpButton
and Combobox.ScrollDownButton
components are used to render the scroll up and down buttons when the select content is larger than the viewport.
You must use the Combobox.Viewport
component when using the scroll buttons.
Custom Scroll Delay
The initial and subsequent scroll delays can be controlled using the delay
prop on the buttons.
For example, we can use the cubicOut
easing function from Svelte to create a smooth scrolling effect that speeds up over time.
<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";
import CaretDoubleUp from "phosphor-svelte/lib/CaretDoubleUp";
import CaretDoubleDown from "phosphor-svelte/lib/CaretDoubleDown";
import { cubicOut } from "svelte/easing";
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" }
];
// Duplicate the menu items a couple of times to show off scrolling a big list
const baseFruits = [...fruits];
for (let i = 0; i < 10; i++) {
for (let baseTheme of baseFruits) {
fruits.push({ ...baseTheme, value: baseTheme.value + i });
}
}
let searchValue = $state("");
const filteredFruits = $derived(
searchValue === ""
? fruits
: fruits.filter((fruit) =>
fruit.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
function autoScrollDelay(tick: number) {
const maxDelay = 200;
const minDelay = 25;
const steps = 30;
const progress = Math.min(tick / steps, 1);
// Use the cubicOut easing function from svelte/easing
return maxDelay - (maxDelay - minDelay) * cubicOut(progress);
}
</script>
<Combobox.Root
type="multiple"
name="favoriteFruit"
onOpenChange={(o) => {
if (!o) searchValue = "";
}}
>
<div class="relative">
<OrangeSlice
class="text-muted-foreground absolute start-3 top-1/2 size-6 -translate-y-1/2"
/>
<Combobox.Input
oninput={(e) => (searchValue = e.currentTarget.value)}
class="h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm"
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="text-muted-foreground size-6" />
</Combobox.Trigger>
</div>
<Combobox.Portal>
<Combobox.Content
class="focus-override border-muted bg-background shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden z-50 h-96 max-h-[var(--bits-combobox-content-available-height)] w-[var(--bits-combobox-anchor-width)] min-w-[var(--bits-combobox-anchor-width)] select-none rounded-xl border px-1 py-3 data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
sideOffset={10}
>
<Combobox.ScrollUpButton
class="flex w-full items-center justify-center py-1"
delay={autoScrollDelay}
>
<CaretDoubleUp class="size-3" />
</Combobox.ScrollUpButton>
<Combobox.Viewport class="p-1">
{#each filteredFruits as fruit, i (i + fruit.value)}
<Combobox.Item
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 w-full select-none items-center py-3 pl-5 pr-1.5 text-sm capitalize"
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, try again.
</span>
{/each}
</Combobox.Viewport>
<Combobox.ScrollDownButton
class="flex w-full items-center justify-center py-1"
delay={autoScrollDelay}
>
<CaretDoubleDown class="size-3" />
</Combobox.ScrollDownButton>
</Combobox.Content>
</Combobox.Portal>
</Combobox.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Native Scrolling/Overflow
If you don't want to use the scroll buttons and prefer to use the standard scrollbar/overflow behavior, you can omit the Combobox.Scroll[Up|Down]Button
components and the Combobox.Viewport
component.
You'll need to set a height on the Combobox.Content
component and appropriate overflow
styles to enable scrolling.
Scroll Lock
To prevent the user from scrolling outside of the Combobox.Content
component when open, you can set the preventScroll
prop to true
.
<Combobox.Content preventScroll={true}>
<!-- ... -->
</Combobox.Content>
Highlighted Items
The Combobox component follows the WAI-ARIA descendant pattern for highlighting items. This means that the Combobox.Input
retains focus the entire time, even when navigating with the keyboard, and items are highlighted as the user navigates them.
Styling Highlighted Items
You can use the data-highlighted
attribute on the Combobox.Item
component to style the item differently when it is highlighted.
onHighlight / onUnhighlight
To trigger side effects when an item is highlighted or unhighlighted, you can use the onHighlight
and onUnhighlight
props.
<Combobox.Item onHighlight={() => console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} />
<!-- ... -->
</Combobox.Item>
Svelte Transitions
You can use the forceMount
prop along with the child
snippet to forcefully mount the Combobox.Content
component to use Svelte Transitions or another animation library that requires more control.
<script lang="ts">
import { Combobox } from "bits-ui";
import { fly } from "svelte/transition";
</script>
<Combobox.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly>
<!-- ... -->
</div>
</div>
{/if}
{/snippet}
</Combobox.Content>
Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the Transitions documentation.
<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";
import CaretDoubleUp from "phosphor-svelte/lib/CaretDoubleUp";
import CaretDoubleDown from "phosphor-svelte/lib/CaretDoubleDown";
import { fly } from "svelte/transition";
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="text-muted-foreground absolute start-3 top-1/2 size-6 -translate-y-1/2"
/>
<Combobox.Input
oninput={(e) => (searchValue = e.currentTarget.value)}
class="h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm"
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="text-muted-foreground size-6" />
</Combobox.Trigger>
</div>
<Combobox.Portal>
<Combobox.Content
class="border-muted bg-background shadow-popover outline-hidden h-96 max-h-[var(--bits-combobox-content-available-height)] w-[var(--bits-combobox-anchor-width)] min-w-[var(--bits-combobox-anchor-width)] rounded-xl border px-1 py-3"
sideOffset={10}
forceMount
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 300 }}>
<Combobox.ScrollUpButton
class="flex w-full items-center justify-center"
>
<CaretDoubleUp class="size-3" />
</Combobox.ScrollUpButton>
<Combobox.Viewport class="p-1">
{#each filteredFruits as fruit, i (i + fruit.value)}
<Combobox.Item
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 w-full select-none items-center py-3 pl-5 pr-1.5 text-sm capitalize"
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, try again.
</span>
{/each}
</Combobox.Viewport>
<Combobox.ScrollDownButton
class="flex w-full items-center justify-center"
>
<CaretDoubleDown class="size-3" />
</Combobox.ScrollDownButton>
</div>
</div>
{/if}
{/snippet}
</Combobox.Content>
</Combobox.Portal>
</Combobox.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
API Reference
The root combobox component which manages & scopes the state of the combobox.
Property | Details |
---|---|
type | |
value | |
onValueChange | |
open | |
onOpenChange | |
onOpenChangeComplete | |
disabled | |
name | |
required | |
scrollAlignment | |
loop | |
allowDeselect | |
items | |
inputValue | |
children |
Data Attribute | Details |
---|
A button which toggles the combobox's open state.
Property | Details |
---|---|
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-state | |
data-disabled | |
data-combobox-trigger |
An optional element to track the scroll position of the combobox for rendering the scroll up/down buttons.
Property | Details |
---|---|
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-combobox-viewport |
The element which contains the combobox's items.
Property | Details |
---|---|
side | |
sideOffset | |
align | |
alignOffset | |
arrowPadding | |
avoidCollisions | |
collisionBoundary | |
collisionPadding | |
sticky | |
hideWhenDetached | |
updatePositionStrategy | |
strategy | |
preventScroll | |
customAnchor | |
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
preventOverflowTextSelection | |
dir | |
loop | |
forceMount | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-state | |
data-combobox-content |
CSS Variable | Details |
---|---|
--bits-combobox-content-transform-origin | |
--bits-combobox-content-available-width | |
--bits-combobox-content-available-height | |
--bits-combobox-anchor-width | |
--bits-combobox-anchor-height |
The element which contains the combobox's items. (Static/No Floating UI)
Property | Details |
---|---|
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
preventScroll | |
preventOverflowTextSelection | |
dir | |
loop | |
forceMount | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-state | |
data-combobox-content |
When used, will render the combobox content into the body or custom to
element when open
Property | Details |
---|---|
to | |
disabled | |
children |
Data Attribute | Details |
---|
A combobox item, which must be a child of the Combobox.Content
component.
Property | Details |
---|---|
value | |
label | |
disabled | |
onHighlight | |
onUnhighlight | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-value | |
data-label | |
data-disabled | |
data-highlighted | |
data-selected | |
data-combobox-item |
A representation of the combobox input element, which is typically displayed in the content.
Property | Details |
---|---|
defaultValue | |
clearOnDeselect | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-state | |
data-disabled | |
data-combobox-input |
A group of related combobox items.
Property | Details |
---|---|
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-combobox-group |
A heading for the parent combobox group. This is used to describe a group of related combobox items.
Property | Details |
---|---|
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-combobox-group-heading |
An optional scroll up button element to improve the scroll experience within the combobox. Should be used in conjunction with the Combobox.Viewport
component.
Property | Details |
---|---|
delay | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-combobox-scroll-up-button |
An optional scroll down button element to improve the scroll experience within the combobox. Should be used in conjunction with the Combobox.Viewport
component.
Property | Details |
---|---|
delay | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-combobox-scroll-down-button |
An optional arrow element which points to the content when open.
Property | Details |
---|---|
width | |
height | |
ref | |
children | |
child |
Data Attribute | Details |
---|---|
data-arrow |