component
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).
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.
---
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>Set selectionMode="multiple". Each chosen value renders as a removable chip (Group → Badge + a dismiss Button) inside the trigger; the popup stays open after each pick. Backspace removes the last chip.
---
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>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).
---
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>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.
---
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>Pass a leading snippet (Svelte) or leading slot (Astro) to render an icon inside the trigger.
---
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>Group related options with section nodes (type: "section").
---
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>Mark an option disabled, or list its key in disabledKeys. Disabled options are skipped by arrow navigation and can’t be selected.
---
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>Three sizes via the size prop: sm, md (default), and lg.
---
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>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.
---
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>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).
---
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>Focus stays on the trigger throughout.
| Key | Action |
|---|---|
↓ / ↑ | Open the popup (when closed), then move the active option |
Enter / Space | Open the popup, or select the active option |
Home / End | Jump to the first / last option |
| Type a letter | Open the popup and jump to a matching option (typeahead) |
Escape | Close the popup (focus stays on the trigger) |
Backspace | Remove the last chip (multiselect) |
Tab | Move focus away and close the popup |
role="combobox" with aria-haspopup="listbox", aria-expanded, and aria-controls (the listbox id).aria-activedescendant; DOM focus never leaves the trigger.role="listbox" of role="option" rows with aria-selected.tabindex="-1"); chip remove buttons are labelled Remove <value>.surface.groundBackground fill for the trigger.
Truncate inline chip labels to this many characters (appending `…`).
CSS `max-width` (e.g. `"8rem"`) applied to inline chip labels with ellipsis truncation.
2Cap 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.
falseShow a clear (×) button to reset the selection when something is selected.
Initial selection (uncontrolled). Single: a key or `null`; multiple: keys.
falseDisable the whole select: the trigger leaves the tab order (`aria-disabled` + `tabindex="-1"`) and ignores pointer/keyboard input.
Keys that cannot be selected or focused.
Bindable reference to the focusable trigger `<div>`.
Content shown when there are no options.
neutralValidation feedback styling for the trigger.
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>`.
falseStretch the trigger to fill its container.
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.
falseShow the loading row instead of options (for async fetches).
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 content inside the trigger (e.g. an icon).
Content shown while `isLoading`.
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.
Fires when the open state changes.
Fires when the selection changes.
Bindable open state.
Per-slot style / attribute override bag for customizing any rendered slot or composed child component.
Placeholder text shown in the trigger when nothing is selected.
"bottom-start"Floating-UI placement of the popup.
falseMarks the control required for native form validation (on the hidden `<select>`).
"single"`"single"` commits one value and closes; `"multiple"` toggles removable chips and keeps the popup open.
"md"Trigger and option size.
Bindable current selection. Single: a key or `null`; multiple: an array of keys.
Plus all standard <div> HTML
attributes.