Skip to content
Areia
Esc
navigateopen⌘Jpreview
On this page

Combobox

A searchable select component for filtering and choosing from a list of options.

Source Code

Areia’s Combobox is built on @data-slot/combobox and rendered as an Ilha island when you use Combobox. It supports keyboard navigation, filtering, form integration, disabled options, grouped items, and field helpers for labels, descriptions, and errors.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="fruit"
    label="Fruit"
    placeholder="Search fruit..."
    items={{
      apple: "Apple",
      banana: "Banana",
      cherry: "Cherry",
      date: "Date",
      elderberry: "Elderberry",
    }}
  />
));

Import

import { Combobox } from "areia";

Usage

Call Combobox(...) for an interactive island. The root renders the combobox markup and initializes the @data-slot/combobox controller after mount.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="ilha-topic"
    label="Ilha topic"
    placeholder="Search Ilha topics..."
    items={{
      islands: "Islands",
      signals: "Signals",
      jsx: "JSX rendering",
      html: "html literals",
      hydration: "Hydration",
    }}
  />
));

For static server-rendered markup without mounting behavior, use Combobox.Static(...). For most application usage, call Combobox(...).

Examples

Basic

A combobox with a visible label and searchable options.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="basic-fruit"
    label="Fruit"
    placeholder="Choose a fruit"
    items={{
      apple: "Apple",
      banana: "Banana",
      cherry: "Cherry",
      date: "Date",
      elderberry: "Elderberry",
    }}
  />
));

Sizes

Use the size prop to match Input sizing: xs, sm, base, and lg.

import { ilha } from "ilha";
import { Combobox } from "areia";

const items = {
  apple: "Apple",
  banana: "Banana",
  cherry: "Cherry",
};

export default ilha.render(() => (
  <div class="flex w-full max-w-sm flex-col gap-3">
    <Combobox
      size="xs"
      placeholder="Extra small"
      items={items}
    />
    <Combobox size="sm" placeholder="Small" items={items} />
    <Combobox size="base" placeholder="Base" items={items} />
    <Combobox size="lg" placeholder="Large" items={items} />
  </div>
));

With Description

The label, description, and error props enable the built-in field wrapper.

Choose the region closest to your users.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="region"
    label="Region"
    description="Choose the region closest to your users."
    placeholder="Search regions..."
    items={{
      iad: "Washington, D.C.",
      sfo: "San Francisco",
      lhr: "London",
      fra: "Frankfurt",
      nrt: "Tokyo",
    }}
  />
));

With Error

Pass error as a string for simple validation messages. Error styling is automatically applied when error is truthy.

Select a database before continuing.
import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="database-error"
    label="Database"
    required
    error="Select a database before continuing."
    placeholder="Search databases..."
    items={{
      postgres: "PostgreSQL",
      mysql: "MySQL",
      mongodb: "MongoDB",
      redis: "Redis",
    }}
  />
));

Disabled

Set disabled to prevent interaction.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    label="Fruit"
    placeholder="Search fruit..."
    disabled
    items={{
      apple: "Apple",
      banana: "Banana",
      cherry: "Cherry",
    }}
  />
));

Disabled Items

Use item descriptor objects to disable individual options. Disabled rows are skipped during keyboard navigation and cannot be selected.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="plans"
    label="Plan"
    placeholder="Search plans..."
    items={{
      free: "Free",
      pro: "Pro",
      business: { label: "Business", disabled: true },
      enterprise: { label: "Enterprise", disabled: true },
    }}
  />
));

Default Value

Use defaultValue with the option value string to preselect an item.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="default-database"
    label="Database"
    defaultValue="postgres"
    items={{
      postgres: "PostgreSQL",
      mysql: "MySQL",
      mongodb: "MongoDB",
      redis: "Redis",
    }}
  />
));

Multiple Selection

Set multiple to allow selecting several values. Selected options render as removable Badge chips inside the input. Clicking an item toggles its selection and keeps the popup open. The typed search text clears after each selection, and Backspace with an empty input removes the last selected value. defaultValue accepts an array, and onValueChange receives a string[].

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="multi-frameworks"
    label="Frameworks"
    multiple
    placeholder="Search frameworks..."
    defaultValue={["astro", "svelte"]}
    items={{
      astro: "Astro",
      next: "Next.js",
      nuxt: "Nuxt",
      remix: "Remix",
      svelte: "SvelteKit",
    }}
  />
));

With name, one hidden input is created per selected value, so a native form submits fruits=apple&fruits=banana.

The committed selection can be bound with bind:value (an alias for bind:group). Types discriminate on multiple: in multiple mode bind:value accepts a string[] signal, defaultValue is string[], and onValueChange receives string[]; in single mode they stay string-typed.

Form Integration

Set name to create a hidden input that submits the selected value with a native form.

import { ilha } from "ilha";
import { Button, Combobox } from "areia";

export default ilha.render(() => (
  <form class="flex w-full max-w-sm flex-col gap-3">
    <Combobox
      id="form-fruit"
      name="fruit"
      label="Fruit"
      required
      placeholder="Search fruit..."
      items={{
        apple: "Apple",
        banana: "Banana",
        cherry: "Cherry",
      }}
    />
    <Button type="submit">Submit</Button>
  </form>
));

Custom Items

Use explicit child markup when you need custom labels, grouping, separators, or item attributes.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="custom-items"
    label="Runtime"
    placeholder="Search runtimes..."
  >
    <Combobox.Item value="bun" label="Bun">
      <span class="font-medium">Bun</span>
      <span class="text-areia-subtle"> JavaScript runtime</span>
    </Combobox.Item>
    <Combobox.Item value="node" label="Node.js">
      <span class="font-medium">Node.js</span>
      <span class="text-areia-subtle"> JavaScript runtime</span>
    </Combobox.Item>
    <Combobox.Item value="deno" label="Deno">
      <span class="font-medium">Deno</span>
      <span class="text-areia-subtle"> TypeScript runtime</span>
    </Combobox.Item>
  </Combobox>
));

Grouped Items

Use Combobox.Group and Combobox.GroupLabel to organize related options.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="grouped-location"
    label="Location"
    placeholder="Search locations..."
  >
    <Combobox.Group>
      <Combobox.GroupLabel>North America</Combobox.GroupLabel>
      <Combobox.Item value="iad">
        Washington, D.C.
      </Combobox.Item>
      <Combobox.Item value="sfo">San Francisco</Combobox.Item>
    </Combobox.Group>
    <Combobox.Group>
      <Combobox.GroupLabel>Europe</Combobox.GroupLabel>
      <Combobox.Item value="lhr">London</Combobox.Item>
      <Combobox.Item value="fra">Frankfurt</Combobox.Item>
    </Combobox.Group>
  </Combobox>
));

Search Input Inside Popup

Use the low-level parts to render a select-like trigger with a separate search input inside the popup. This composition returns static markup; initialize it with createCombobox yourself if you are not using Combobox.

import { ilha } from "ilha";
import { Combobox } from "areia";

const items = [
  <Combobox.Item value="go" label="Go" children="🐹 Go" />,
  <Combobox.Item
    value="rust"
    label="Rust"
    children="🦀 Rust"
  />,
  <Combobox.Item
    value="ts"
    label="TypeScript"
    children="🔷 TypeScript"
  />,
];

export default ilha.render(() => (
  <Combobox
    id="language-inside"
    label="Language"
    placeholder="Select language"
  >
    <Combobox.TriggerValue placeholder="Select language" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search languages..." />
      <Combobox.List>
        <Combobox.Empty />
        {items}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
));

Open on Focus and Auto Highlight

openOnFocus opens the popup when the input receives intentional focus. autoHighlight highlights the first matching item after typing.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="quick-search"
    label="Quick search"
    placeholder="Start typing..."
    openOnFocus
    autoHighlight
    items={{
      dashboard: "Dashboard",
      settings: "Settings",
      billing: "Billing",
      support: "Support",
    }}
  />
));

Autocomplete Behavior

Combobox supports autocomplete-style filtering natively. As the user types, the dropdown filters visible items automatically using the built-in substring matcher. Provide items and the controller handles narrowing without re-rendering.

import { ilha } from "ilha";
import { Combobox } from "areia";

export default ilha.render(() => (
  <Combobox
    id="autocomplete"
    label="Language"
    placeholder="Start typing..."
    openOnFocus
    items={{
      rust: "Rust",
      ruby: "Ruby",
      react: "React",
      python: "Python",
      php: "PHP",
      perl: "Perl",
    }}
  />
));

For a custom filter, pass a filter function instead of re-rendering with filtered items.

filter={(inputValue, itemValue, itemLabel) =>
  itemLabel.toLowerCase().startsWith(inputValue.toLowerCase())
}

Use onInputValueChange to read the typed text for side effects like remote fetching. Do not use it to drive re-renders of the item list — the controller already filters internally.

For free-form values that do not need to match the list, use itemToStringValue to control how the selected text is displayed, or treat the input value as the committed value when the user submits the form without selecting an item.

Combobox.Content defaults to 24rem or the available viewport height, whichever is smaller. Pass class or className when composing the low-level content yourself.

import { ilha } from "ilha";
import { Combobox } from "areia";

const items = {
  one: "One",
  two: "Two",
  three: "Three",
  four: "Four",
  five: "Five",
};

export default ilha.render(() => (
  <Combobox
    id="short-dropdown"
    label="Short dropdown"
    placeholder="Search..."
  >
    <Combobox.TriggerInput placeholder="Search..." />
    <Combobox.Content class="max-h-40">
      <Combobox.List>
        <Combobox.Empty />
        {Object.entries(items).map(([value, label]) => (
          <Combobox.Item value={value}>{label}</Combobox.Item>
        ))}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
));

API Reference

Combobox

Combobox is an Ilha island. It accepts standard HTML div attributes plus @data-slot/combobox options and Areia field props.

Prop Type Default Description
class string - Additional CSS classes applied to the combobox root.
className string - Alias for class.
size "xs" | "sm" | "base" | "lg" "base" Size of the trigger input. Matches Input component sizes.
inputSide "right" | "top" "right" Styling hook for chip/input layouts.
label unknown - Label content. Enables the field wrapper.
labelTooltip string - Tooltip text rendered as a native title on the label.
description unknown - Helper text displayed below the combobox.
error unknown | { message: unknown; match?: unknown } - Error message. When truthy, error styling is automatically applied.
items Record<string, unknown | { label: unknown; disabled?: boolean }> | ComboboxItemDescriptor[] - Items rendered as selectable options. Object keys are option values.
children unknown - Explicit combobox content. Prefer the second Combobox argument.
placeholder string - Placeholder text for the trigger input or trigger value.
multiple boolean false Allow selecting multiple values. Items toggle and the popup stays open.
defaultValue string | string[] - Initial selected value(s). Array form is for multiple mode.
defaultOpen boolean false Initial open state.
disabled boolean false Disable interaction.
required boolean false Mark the field as required. When false with a label, shows optional text.
name string - Form field name. Creates a hidden input for submission.
openOnFocus boolean true Open the popup when the input receives intentional focus.
autoHighlight boolean false Auto-highlight the first visible item after typing.
filter (inputValue: string, itemValue: string, itemLabel: string) => boolean - Custom filter function. Return true to show an item.
itemToStringValue (item: HTMLElement | null, value: string | null) => string - Custom selected-value text resolver.
onValueChange (value: string | string[] | null) => void - Called when selection changes. Receives string[] in multiple mode.
onOpenChange (open: boolean) => void - Called when popup opens or closes.
onInputValueChange (inputValue: string) => void - Called when the user types in the input.
side "top" | "bottom" "bottom" Preferred popup side.
align "start" | "center" | "end" "start" Popup alignment relative to the trigger.
sideOffset number 4 Distance from the trigger in pixels.
alignOffset number 0 Offset from the aligned edge in pixels.
avoidCollisions boolean true Adjust popup position to stay inside the viewport.
collisionPadding number 8 Viewport edge padding used for collision detection.

Combobox.Item

Individual selectable option.

Prop Type Default Description
value string - Submitted and selected value for the item.
children unknown - Visible item content.
label string - Text used for filtering when children are custom.
disabled boolean - Whether the item cannot be selected.
class string - Additional CSS classes.
className string - Alias for class.

Additional Sub-components

Component Description
Combobox.TriggerInput Input trigger with clear and dropdown buttons.
Combobox.TriggerValue Button trigger that displays the selected value.
Combobox.Content Popup container for the input and list.
Combobox.Input Search input for popup-input composition.
Combobox.List Scrollable list wrapper.
Combobox.Empty Empty state shown when no items match.
Combobox.Group Group container for related items.
Combobox.GroupLabel Visible label for a group.
Combobox.Separator Visual separator between groups/items.
Combobox.Chip Visual chip helper for custom multi-value interfaces.

Accessibility

Keyboard Navigation

Key Action
ArrowDown Open the popup when closed, or move to the next visible item.
ArrowUp Open the popup when closed, or move to the previous item.
Home Move to the first visible item.
End Move to the last visible item.
Enter Select the highlighted item.
Escape Close the popup, or clear the selected value(s) when closed.
Backspace In multiple mode with an empty input, remove the last selected value.
Tab Close the popup and continue normal tab navigation.

Label Requirement

Comboboxes should have an accessible name via one of:

  • label prop
  • aria-label on a custom Combobox.TriggerInput
  • aria-labelledby for custom label association

Form Behavior

When name is provided, the controller creates a hidden input with the selected value so the combobox participates in native form submission. In multiple mode, one hidden input is created per selected value.

Error Association

When an id is provided, descriptions and error messages are automatically associated with the combobox field wrapper using aria-describedby.

Was this page helpful?