Siteplane UI

Select

Choose from a fixed set of options without text filtering.

Markdown
Workspace
"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

pnpm dlx shadcn@4.18.0 add @siteplane/select

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Field is installed with Input.

Usage

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

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. The item value remains the value used for selection and native form submission.

Workspace
"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 check does, and appears instantly under reduced motion.

Teams
"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
"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
"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.

Workspace, default
Workspace, elevated
Workspace, ghost
Workspace, inset
Workspace, disabled
"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.

Workspace, small
Workspace, medium
Workspace, large
Workspace, extra large
"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. 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
"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

Component properties
PropTypeDefaultDescription
Select.itemsRecord<string, ReactNode> | { value, label }[] | grouped itemsunsetMap stored values to SelectValue labels. Supply when IDs differ from display text.
SelectValue.childrenReactNode | (value) => ReactNodelabel resolved from items, otherwise raw valueOverride selected-value display without changing the stored or submitted value.
SelectTrigger.size / SelectButton.sizeSelectTriggerSize: sm | md | lg | xlprovider / lgControl dimensions.
SelectTrigger.variant / SelectButton.variantSelectTriggerMode: default | elevated | ghost | insetdefaultTrigger 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 / alignPositioner side / alignmentbottom / startPreferred position.
SelectPopup.sideOffset / alignOffsetPositioner offsets8 / 0Gap and alignment offset.
SelectPopup.alignItemWithTriggerbooleanfalseAlign selected item with the trigger.
SelectPopup.anchorPositioner anchortriggerCustom anchor.
SelectPopup.portalPropsPortal.PropsunsetPortal options.
Select.value / defaultValueValue or Value[] in multiple modeuncontrolledSelection.
Select.onValueChange(value, eventDetails) => voidunsetSelection callback.
Select.onOpenChange(open, eventDetails) => voidunsetVisibility 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. SelectValue, Item, Group, GroupLabel, Label and Separator wrap corresponding Base UI 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.

Usage notes

Arrow keys and typing navigate options; Enter selects and Escape closes. Use Combobox for selection with search, or Autocomplete for text that may differ from suggestions. A placeholder is not an accessible label.