# Select

Choose from a fixed set of options without text filtering.

Workspace select

[Complete TSX file](/examples/select/default.tsx)

```tsx
"use client";

import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const items = [
  { value: "design", label: "Design team" },
  { value: "engineering", label: "Engineering team" },
  { value: "archive", label: "Archived workspace", disabled: true },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Select items={items}>
        <SelectLabel>Workspace</SelectLabel>
        <SelectTrigger>
          <SelectValue placeholder="Choose workspace" />
        </SelectTrigger>
        <SelectPopup>
          {items.map((item) => (
            <SelectItem
              key={item.value}
              value={item.value}
              disabled={item.disabled}
            >
              {item.label}
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
    </div>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/select
```

With npm: `npx shadcn@4.18.0 add @siteplane/select`. [Registry JSON](/registry/select.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

<Select items={[{ value: "design", label: "Design team" }]}>
  <SelectTrigger aria-label="Workspace">
    <SelectValue placeholder="Choose workspace" />
  </SelectTrigger>
  <SelectPopup>
    <SelectItem value="design">Design team</SelectItem>
  </SelectPopup>
</Select>;
```

## Composition

```text
Select
├── SelectLabel
├── SelectTrigger → SelectValue
└── SelectPopup / SelectContent
    ├── SelectGroup → SelectGroupLabel + SelectItem
    └── SelectSeparator
```

## Controlled selection

`value` stores the option ID, such as `"design"`; `items` maps it to the displayed label, such as `"Design team"`. Pass the mapping to `Select` so a preselected value displays its label before the popup opens. Disabled items cannot be selected.

Alternatively, provide the display label explicitly as `SelectValue` children, as shown in [Team settings](/docs/examples/team-settings/). The item value remains the value used for selection and native form submission.

Controlled selection

[Complete TSX file](/examples/select/controlled.tsx)

```tsx
"use client";

import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";
import { useState } from "react";

const items = [
  { value: "design", label: "Design team" },
  { value: "engineering", label: "Engineering team" },
  { value: "archive", label: "Archived workspace", disabled: true },
];

export default function Example() {
  const [value, setValue] = useState<string | null>("design");
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Select items={items} value={value} onValueChange={setValue}>
        <SelectLabel>Workspace</SelectLabel>
        <SelectTrigger>
          <SelectValue placeholder="Choose workspace" />
        </SelectTrigger>
        <SelectPopup>
          {items.map((item) => (
            <SelectItem
              key={item.value}
              value={item.value}
              disabled={item.disabled}
            >
              {item.label}
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
    </div>
  );
}
```

## Multiple selection

Set `multiple` to let people choose several options; `value` and `defaultValue` become arrays. Pass a function to `SelectValue` to summarize a long selection in the trigger. The check of a selected item draws in when the item is picked and out when it is not, the way the [Checkbox](/docs/components/checkbox/) check does, and appears instantly under reduced motion.

Team multi-select

[Complete TSX file](/examples/select/multiple.tsx)

```tsx
"use client";

import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const teams = {
  engineering: "Engineering",
  design: "Design",
  product: "Product",
  marketing: "Marketing",
  sales: "Sales",
  support: "Support",
};

type Team = keyof typeof teams;

const values = Object.keys(teams) as Team[];

function renderTeams(value: Team[]) {
  const [first] = value;
  if (!first) return "Choose teams";
  const more = value.length > 1 ? ` (+${value.length - 1} more)` : "";
  return teams[first] + more;
}

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Select multiple defaultValue={["design", "product"]}>
        <SelectLabel>Teams</SelectLabel>
        <SelectTrigger>
          <SelectValue>{renderTeams}</SelectValue>
        </SelectTrigger>
        <SelectPopup>
          {values.map((value) => (
            <SelectItem key={value} value={value}>
              {teams[value]}
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
    </div>
  );
}
```

## Groups

Group related options with `SelectGroup` and `SelectGroupLabel`, and divide groups with `SelectSeparator`. `SelectLabel` still names the whole control.

Data region select with groups

[Complete TSX file](/examples/select/groups.tsx)

```tsx
"use client";

import {
  Select,
  SelectGroup,
  SelectGroupLabel,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const regions = [
  {
    label: "Americas",
    items: [
      { value: "us", label: "United States" },
      { value: "ca", label: "Canada" },
      { value: "br", label: "Brazil" },
    ],
  },
  {
    label: "Europe",
    items: [
      { value: "de", label: "Germany" },
      { value: "ie", label: "Ireland" },
    ],
  },
  {
    label: "Asia Pacific",
    items: [
      { value: "sg", label: "Singapore" },
      { value: "jp", label: "Japan" },
      { value: "au", label: "Australia" },
    ],
  },
];

const items = regions.flatMap((region) => region.items);

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Select items={items}>
        <SelectLabel>Data region</SelectLabel>
        <SelectTrigger>
          <SelectValue placeholder="Choose a region" />
        </SelectTrigger>
        <SelectPopup>
          {regions.map((region, index) => (
            <SelectGroup key={region.label}>
              {index > 0 && <SelectSeparator />}
              <SelectGroupLabel>{region.label}</SelectGroupLabel>
              {region.items.map((item) => (
                <SelectItem key={item.value} value={item.value}>
                  {item.label}
                </SelectItem>
              ))}
            </SelectGroup>
          ))}
        </SelectPopup>
      </Select>
    </div>
  );
}
```

## Options with icons

Use object items when options carry an icon or other data. A `SelectValue` function renders the selected object in the trigger, and `itemToStringValue` supplies the value submitted with a form.

Visibility select with icons

[Complete TSX file](/examples/select/options-with-icon.tsx)

```tsx
"use client";

import { GlobeIcon, LockIcon, UsersIcon } from "lucide-react";
import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const visibilityOptions = [
  { icon: GlobeIcon, label: "Public", value: "public" },
  { icon: UsersIcon, label: "Workspace", value: "workspace" },
  { icon: LockIcon, label: "Private", value: "private" },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Select
        defaultValue={visibilityOptions[1]}
        itemToStringValue={(option) => option.value}
      >
        <SelectLabel>Visibility</SelectLabel>
        <SelectTrigger>
          <SelectValue>
            {(option) => (
              <span className="flex min-w-0 items-center gap-2">
                <option.icon />
                <span className="truncate">{option.label}</span>
              </span>
            )}
          </SelectValue>
        </SelectTrigger>
        <SelectPopup>
          {visibilityOptions.map((option) => (
            <SelectItem key={option.value} value={option}>
              <span className="flex min-w-0 items-center gap-2">
                <option.icon />
                <span className="truncate">{option.label}</span>
              </span>
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
    </div>
  );
}
```

## Variants and disabled

`variant` belongs to `SelectTrigger`, not the root. The ghost trigger is for a value inside a row, so it is shown small and as wide as its text. Set `disabled` on `Select` to block interaction.

Select variants and disabled state

[Complete TSX file](/examples/select/appearance.tsx)

```tsx
"use client";

import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  type SelectTriggerMode,
  type SelectTriggerSize,
  SelectValue,
} from "@/siteplane/components/select";

const items = [
  { value: "design", label: "Design team" },
  { value: "engineering", label: "Engineering team" },
  { value: "research", label: "Research team" },
];

const triggers: {
  className?: string;
  disabled?: boolean;
  label: string;
  size?: SelectTriggerSize;
  variant?: SelectTriggerMode;
}[] = [
  { label: "default" },
  { label: "elevated", variant: "elevated" },
  // A ghost trigger sits in a row: small, and as wide as its text.
  { className: "w-fit min-w-0", label: "ghost", size: "sm", variant: "ghost" },
  { label: "inset", variant: "inset" },
  { label: "disabled", disabled: true },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {triggers.map(({ className, disabled, label, size, variant }) => (
        <div className="grid gap-2" key={label}>
          <Select defaultValue="design" disabled={disabled} items={items}>
            <SelectLabel>Workspace, {label}</SelectLabel>
            <SelectTrigger className={className} size={size} variant={variant}>
              <SelectValue />
            </SelectTrigger>
            <SelectPopup>
              {items.map((item) => (
                <SelectItem key={item.value} value={item.value}>
                  {item.label}
                </SelectItem>
              ))}
            </SelectPopup>
          </Select>
        </div>
      ))}
    </div>
  );
}
```

## Sizes

`size` belongs to `SelectTrigger` too. Omit it to inherit the provider's control size.

Select sizes

[Complete TSX file](/examples/select/sizes.tsx)

```tsx
"use client";

import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const items = [
  { value: "design", label: "Design team" },
  { value: "engineering", label: "Engineering team" },
  { value: "research", label: "Research team" },
];

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {sizes.map(({ size, label }) => (
        <div className="grid gap-2" key={size}>
          <Select defaultValue="design" items={items}>
            <SelectLabel>Workspace, {label}</SelectLabel>
            <SelectTrigger size={size}>
              <SelectValue />
            </SelectTrigger>
            <SelectPopup>
              {items.map((item) => (
                <SelectItem key={item.value} value={item.value}>
                  {item.label}
                </SelectItem>
              ))}
            </SelectPopup>
          </Select>
        </div>
      ))}
    </div>
  );
}
```

## Form integration

Set `name` and `required` on `Select` to submit the selected item’s value with a [Form](/docs/components/form/). Inside a `Field`, the Form blocks a submit without a selection and focuses the trigger, and `FieldError` shows the message. Uncontrolled selection resets with the form; controlled state needs your own reset handler.

Workspace select in a form

[Complete TSX file](/examples/select/form.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldError } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import {
  Select,
  SelectItem,
  SelectLabel,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const items = [
  { value: "design", label: "Design team" },
  { value: "engineering", label: "Engineering team" },
  { value: "research", label: "Research team" },
];

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-3"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field>
        <Select items={items} name="workspace" required>
          <SelectLabel>Workspace</SelectLabel>
          <SelectTrigger className="w-full">
            <SelectValue placeholder="Choose workspace" />
          </SelectTrigger>
          <SelectPopup>
            {items.map((item) => (
              <SelectItem key={item.value} value={item.value}>
                {item.label}
              </SelectItem>
            ))}
          </SelectPopup>
        </Select>
        <FieldError match="valueMissing">Choose a workspace.</FieldError>
      </Field>
      <div className="flex gap-2">
        <Button type="submit">Save</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
    </Form>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Select.items | Record<string, ReactNode> \| { value, label }[] \| grouped items | unset | Map stored values to SelectValue labels. Supply when IDs differ from display text. |
| SelectValue.children | ReactNode \| (value) => ReactNode | label resolved from items, otherwise raw value | Override selected-value display without changing the stored or submitted value. |
| SelectTrigger.size / SelectButton.size | SelectTriggerSize: sm \| md \| lg \| xl | provider / lg | Control dimensions. |
| SelectTrigger.variant / SelectButton.variant | SelectTriggerMode: default \| elevated \| ghost \| inset | default | Trigger surface. Ghost draws no field chrome, only the value and caret with hover and pressed levels, for a value that sits in a row. Pair it with size sm and a width that fits the text. |
| SelectPopup.side / align | Positioner side / alignment | bottom / start | Preferred position. |
| SelectPopup.sideOffset / alignOffset | Positioner offsets | 8 / 0 | Gap and alignment offset. |
| SelectPopup.alignItemWithTrigger | boolean | false | Align selected item with the trigger. |
| SelectPopup.anchor | Positioner anchor | trigger | Custom anchor. |
| SelectPopup.portalProps | Portal.Props | unset | Portal options. |
| Select.value / defaultValue | Value or Value[] in multiple mode | uncontrolled | Selection. |
| Select.onValueChange | (value, eventDetails) => void | unset | Selection callback. |
| Select.onOpenChange | (open, eventDetails) => void | unset | Visibility callback. |
Select preserves the generic Base UI Root contract (`Value`, `Multiple`) and
adds a hidden disabled reset marker. SelectTrigger wraps the foundation trigger
with SelectButton, which is also exported for composition. SelectButton alone
supplies appearance and caret, not select behavior; the Combobox page shows it
as the trigger of a [searchable
select](/docs/components/combobox/#searchable-select). SelectValue, Item, Group,
GroupLabel, Label and Separator wrap corresponding [Base UI
Select](https://base-ui.com/react/components/select) parts. Popup includes
Portal, Positioner, scroll arrows and List; SelectContent aliases Popup.
SelectPrimitive is the namespace. SelectButtonProps and the trigger size/mode
types are exported. Styled classes are strings. Item supplies its own
text/indicator, with foundation value/disabled props.

[Read the installed source](/source/select).

## Usage notes

Arrow keys and typing navigate options; Enter selects and Escape closes. Use [Combobox](/docs/components/combobox/) for selection with search, or [Autocomplete](/docs/components/autocomplete/) for text that may differ from suggestions. A placeholder is not an accessible label.
