component

Select

A select-only combobox: a non-editable trigger that shows the current selection (or a placeholder) and opens a ListBox in a Popover to choose from. It pairs the Popover positioning layer and the ListBox option engine behind one headless connectSelect core, so the same component supports single and multiple selection, type-to-jump, removable chips, a clear button, and native form submission. For a plain native <select> element, see Select (native).

Unlike the Combobox, the Select has no text field — you don’t type to filter, you type to jump to a matching option. The trigger is a focusable role="combobox" element (a <div>, so it can hold the chips’ remove buttons) and it keeps DOM focus while driving the list with aria-activedescendant (the ListBox’s focusStrategy="active-descendant" mode).

Default

Single selection. Click the trigger (or press / Enter / Space) to open, arrow keys to move the cursor (focus stays on the trigger), Enter to commit. Selecting an option updates the trigger label and closes the popup.

  • React

  • Svelte

  • Vue

  • Solid

  • Angular

  • Qwik

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
  { id: "solid", label: "Solid" },
  { id: "angular", label: "Angular" },
  { id: "qwik", label: "Qwik" },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-default"
    items={items}
    aria-label="Framework"
    placeholder="Pick a framework…"
  />
</div>

Multiple selection

Set selectionMode="multiple". Each chosen value renders as a removable chip (GroupBadge + a dismiss Button) inside the trigger; the popup stays open after each pick. Backspace removes the last chip.

  • React

  • Svelte

  • Vue

  • Solid

  • Angular

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
  { id: "solid", label: "Solid" },
  { id: "angular", label: "Angular" },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-multi"
    items={items}
    selectionMode="multiple"
    chipOverflow={false}
    defaultValue={["svelte"]}
    aria-label="Frameworks"
    placeholder="Pick frameworks…"
  />
</div>

Chip overflow

When many values are selected the chip row can grow taller than the trigger has room for. Cap it with chipOverflow (defaults to 1):

  • chipOverflow={n} — keep the last n chips inline and collapse the rest behind a “+N” chip.
  • chipOverflow="auto" — measure the trigger and fit as many chips as the width allows (best with a constrained width).
  • chipOverflow={false} — disable collapsing entirely: every chip renders inline.

The “+N” chip opens a popover listing the collapsed options with keyboard-operable remove buttons. chipMaxChars={n} and chipMaxWidth="8rem" trim long labels (the full label stays in the chip’s title).

  • React

  • Svelte

  • Vue

  • Solid

  • Angular

  • Qwik

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
  { id: "solid", label: "Solid" },
  { id: "angular", label: "Angular" },
  { id: "qwik", label: "Qwik" },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-overflow"
    items={items}
    selectionMode="multiple"
    defaultValue={["react", "svelte", "vue"]}
    aria-label="Frameworks"
    placeholder="Pick frameworks…"
  />
</div>

Preselected & clearable

Seed an initial selection with defaultValue (a key for single-select, an array of keys for multiple). Add clearable to show a clear (×) button that resets the selection when something is selected.

  • React

  • Svelte

  • Vue

  • Solid

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
  { id: "solid", label: "Solid" },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-preselected"
    items={items}
    defaultValue="svelte"
    clearable
    aria-label="Framework"
    placeholder="Pick a framework…"
  />
</div>

Leading content

Pass a leading snippet (Svelte) or leading slot (Astro) to render an icon inside the trigger.

  • React

    A library for web and native UIs

  • Svelte

    Cybernetically enhanced web apps

  • Vue

    The progressive framework

  • Solid

    Simple and performant reactivity

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { Layers } from "@lucide/astro";

const items = [
  {
    id: "react",
    label: "React",
    description: "A library for web and native UIs",
  },
  {
    id: "svelte",
    label: "Svelte",
    description: "Cybernetically enhanced web apps",
  },
  { id: "vue", label: "Vue", description: "The progressive framework" },
  {
    id: "solid",
    label: "Solid",
    description: "Simple and performant reactivity",
  },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-icons"
    items={items}
    aria-label="Framework"
    placeholder="Pick a framework…"
  >
    <Layers slot="leading" />
  </Select>
</div>

Sections

Group related options with section nodes (type: "section").

  • Frontend
  • Backend
---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";
import type { ListBoxNodeInput } from "@pindoba/core-select";

const items: ListBoxNodeInput[] = [
  {
    id: "frontend",
    type: "section",
    title: "Frontend",
    items: [
      { id: "react", label: "React" },
      { id: "svelte", label: "Svelte" },
      { id: "vue", label: "Vue" },
    ],
  },
  {
    id: "backend",
    type: "section",
    title: "Backend",
    items: [
      { id: "node", label: "Node.js" },
      { id: "deno", label: "Deno" },
      { id: "bun", label: "Bun" },
    ],
  },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-grouped"
    items={items}
    aria-label="Technology"
    placeholder="Pick a technology…"
  />
</div>

Disabled options

Mark an option disabled, or list its key in disabledKeys. Disabled options are skipped by arrow navigation and can’t be selected.

  • React

  • Svelte

  • Vue

  • Solid

  • Angular

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue", disabled: true },
  { id: "solid", label: "Solid" },
  { id: "angular", label: "Angular", disabled: true },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-disabled"
    items={items}
    disabledKeys={["vue", "angular"]}
    aria-label="Framework"
    placeholder="Pick a framework…"
  />
</div>

Sizes

Three sizes via the size prop: sm, md (default), and lg.

  • React

  • Svelte

  • Vue

  • React

  • Svelte

  • Vue

  • React

  • Svelte

  • Vue

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
];
---

<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-sm"
    items={items}
    size="sm"
    aria-label="Small"
    placeholder="Small…"
  />
  <Select
    id="astro-select-md"
    items={items}
    size="md"
    aria-label="Medium"
    placeholder="Medium…"
  />
  <Select
    id="astro-select-lg"
    items={items}
    size="lg"
    aria-label="Large"
    placeholder="Large…"
  />
</div>

Form submission

Set name and the selection posts inside a <form> via a hidden native <select> — so you also get browser autofill, native constraint validation (required), and write-back (an autofilled value updates the visible trigger). It submits exactly like a native select (one value, or repeated values for multiple). form associates the control with a <form> by id when it lives outside the form element. Very large option lists fall back to hidden <input>s.

  • React

  • Svelte

  • Vue

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
];
---

<form class={stack({ gap: "sm", alignItems: "flex-start" })}>
  <Select
    id="astro-select-form"
    items={items}
    name="framework"
    defaultValue="svelte"
    aria-label="Framework"
    placeholder="Pick a framework…"
  />
  <button type="submit">Submit</button>
</form>

Labeling

The trigger is a <div role="combobox"> (not a labelable element), so the consumer id lands on the hidden native <select> instead. That makes a plain <label for={id}> focus the Select — clicking the label focuses the trigger and paints the ring, without needing the Field wrapper. It focuses the trigger but does not open the dropdown, matching a native <select> in Chrome. Add aria-labelledby pointing at the same label so the role="combobox" trigger gets its screen-reader name (or use Field, which wires both for you).

  • React

  • Svelte

  • Vue

---
import Select from "../select.astro";
import { stack } from "@pindoba/styled-system/patterns";

const items = [
  { id: "react", label: "React" },
  { id: "svelte", label: "Svelte" },
  { id: "vue", label: "Vue" },
];

// The consumer `id` lands on the hidden native <select>, so a plain
// `<label for={id}>` focuses the control (no <Field> needed). `aria-labelledby`
// points the trigger at the same label for the screen-reader name.
const id = "astro-select-labeled";
---

<div class={stack({ gap: "2xs", alignItems: "flex-start" })}>
  <label for={id} id={`${id}-label`}>Framework</label>
  <Select
    id={id}
    items={items}
    aria-labelledby={`${id}-label`}
    placeholder="Pick one…"
  />
</div>

Keyboard

Focus stays on the trigger throughout.

KeyAction
/ Open the popup (when closed), then move the active option
Enter / SpaceOpen the popup, or select the active option
Home / EndJump to the first / last option
Type a letterOpen the popup and jump to a matching option (typeahead)
EscapeClose the popup (focus stays on the trigger)
BackspaceRemove the last chip (multiselect)
TabMove focus away and close the popup

Accessibility

  • The trigger is a role="combobox" with aria-haspopup="listbox", aria-expanded, and aria-controls (the listbox id).
  • The active option is surfaced via aria-activedescendant; DOM focus never leaves the trigger.
  • The popup is a role="listbox" of role="option" rows with aria-selected.
  • The clear button is labelled and removed from the tab order (tabindex="-1"); chip remove buttons are labelled Remove <value>.
props · 29 shown · 29 total
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"
default surface.ground

Background fill for the trigger.

chipMaxChars
number

Truncate inline chip labels to this many characters (appending `…`).

chipMaxWidth
string

CSS `max-width` (e.g. `"8rem"`) applied to inline chip labels with ellipsis truncation.

chipOverflow
numberfalse"auto"
default 2

Cap the inline chip footprint in multiselect. A `number` shows the last N selected chips inline and collapses the rest behind a "+N" overflow chip; `"auto"` measures the trigger and fits as many chips as the width allows; `false` disables collapsing entirely so every chip renders inline.

clearable
boolean
default false

Show a clear (×) button to reset the selection when something is selected.

defaultValue svelte
SelectValue

Initial selection (uncontrolled). Single: a key or `null`; multiple: keys.

disabled
boolean
default false

Disable the whole select: the trigger leaves the tab order (`aria-disabled` + `tabindex="-1"`) and ignores pointer/keyboard input.

disabledKeys svelte
Iterable<Key>

Keys that cannot be selected or focused.

element binding svelte
HTMLDivElementnull

Bindable reference to the focusable trigger `<div>`.

empty slot svelte
Snippet

Content shown when there are no options.

feedback
"primary""neutral""success""warning""danger""inherit"
default neutral

Validation feedback styling for the trigger.

form
string

Associates the control with a `<form>` by id (the native `form` attribute), so it can live outside the form element. Applied to the hidden `<select>`.

fullWidth
boolean
default false

Stretch the trigger to fill its container.

id svelte
string

Consumer id. Lands on the hidden native `<select>` (the labelable form element), so `<label for={id}>` focuses the Select without `<Field>`. Internal ids (trigger/listbox/popover) derive from a stable generated base.

isLoading
boolean
default false

Show the loading row instead of options (for async fetches).

items required
ListBoxNodeInput[]

The options to choose from, in render order. An option is `{ id, label, description?, eyebrow?, leading?, trailing?, textValue?, disabled? }`; a section is `{ id, type: "section", title?, items }`.

leading slot svelte
Snippet

Leading content inside the trigger (e.g. an icon).

loading slot svelte
Snippet

Content shown while `isLoading`.

name
string

Form field name. When set, the selection posts with a surrounding `<form>` via a hidden native `<select>` (autofill + native validation + `FormData`); very large option lists fall back to hidden `<input>`s.

onOpenChange svelte
(open: boolean) => void

Fires when the open state changes.

onValueChange svelte
(value: SelectValue) => void

Fires when the selection changes.

open svelte
boolean

Bindable open state.

passThrough
SelectPassThrough

Per-slot style / attribute override bag for customizing any rendered slot or composed child component.

placeholder
string

Placeholder text shown in the trigger when nothing is selected.

placement
"bottom""left""right""top""bottom-end""bottom-start""left-end""left-start""right-end""right-start""top-end""top-start"
default "bottom-start"

Floating-UI placement of the popup.

required
boolean
default false

Marks the control required for native form validation (on the hidden `<select>`).

selectionMode
"single""multiple"
default "single"

`"single"` commits one value and closes; `"multiple"` toggles removable chips and keeps the popup open.

size
"sm""md""lg"
default "md"

Trigger and option size.

value svelte
SelectValue

Bindable current selection. Single: a key or `null`; multiple: an array of keys.

Plus all standard <div> HTML attributes.

Type

  • Components
  • Blocks