Skip to content
design
Components

Select

A list of choices that opens from a button.

On this page
Who hears the anchor alarm, grouped with icons, roles and an unreachable contact, beside a wind unit.
Code
import { FormField } from "@bosunkit/components/form-field";
import { Reading } from "@bosunkit/components/reading";
import { Select } from "@bosunkit/components/select";
import { Anchor, Compass, LifeBuoy, Phone, Radio, Wind, type LucideIcon } from "lucide-react";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Who hears the anchor alarm, grouped by watch with icons, roles and a contact that cannot be reached, opening on " +
  "a placeholder and an error until someone is chosen, beside a live wind reading whose unit a popper select sets.";

interface Contact {
  readonly value: string;
  readonly name: string;
  readonly detail: string;
  readonly icon: LucideIcon;
  readonly disabled?: boolean | undefined;
}

const ON_WATCH: readonly Contact[] = [
  { value: "marta", name: "Marta Lindqvist", detail: "Watch leader, cockpit speaker", icon: Compass },
  { value: "joost", name: "Joost Bakker", detail: "Navigator, nav station", icon: Radio },
];

const OFF_WATCH: readonly Contact[] = [
  { value: "anneke", name: "Anneke de Vries", detail: "Skipper, aft cabin speaker", icon: Anchor },
  { value: "femke", name: "Femke Smit", detail: "Engineer, forward cabin", icon: LifeBuoy },
];

const SHORE: readonly Contact[] = [
  {
    value: "harbor",
    name: "Camaret harbor office",
    detail: "No mobile signal at anchor since 01:20",
    icon: Phone,
    disabled: true,
  },
];

type WindUnit = "kn" | "m/s" | "km/h";

const PER_KNOT: Readonly<Record<WindUnit, number>> = { kn: 1, "m/s": 0.514444, "km/h": 1.852 };
/** True wind in knots, one reading every two seconds. */
const WIND_KNOTS: readonly number[] = [22.4, 23.1, 24.6, 26.2, 25.0, 23.8, 24.4, 27.1];
const WIND_MILLISECONDS: number = 2000;

const ROW: CSSProperties = { display: "inline-flex", alignItems: "center", gap: "var(--space-sm)" };
const DETAIL: CSSProperties = { marginInlineStart: "var(--space-sm)", color: "var(--text-dim)", fontSize: 11 };

function contactItem(contact: Contact): ReactElement {
  return (
    <Select.Item key={contact.value} value={contact.value} disabled={contact.disabled}>
      <span style={ROW}>
        <contact.icon size={14} aria-hidden="true" />
        {contact.name}
        <span style={DETAIL}>{contact.detail}</span>
      </span>
    </Select.Item>
  );
}

export default function Showcase(): ReactElement {
  const [contact, setContact]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  const [unit, setUnit]: [WindUnit, Dispatch<SetStateAction<WindUnit>>] = useState<WindUnit>("kn");
  const [tick, setTick]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);

  useEffect((): (() => void) => {
    const timer: number = window.setInterval((): void => {
      setTick((current: number): number => current + 1);
    }, WIND_MILLISECONDS);
    return (): void => {
      window.clearInterval(timer);
    };
  }, []);

  const chosen: Contact | undefined = [...ON_WATCH, ...OFF_WATCH].find(
    (candidate: Contact): boolean => candidate.value === contact,
  );
  const knots: number = WIND_KNOTS[tick % WIND_KNOTS.length] ?? 0;
  const wind: number = knots * PER_KNOT[unit];

  return (
    <div
      style={{
        width: "100%",
        maxWidth: 600,
        display: "grid",
        gridTemplateColumns: "minmax(0, 1fr) auto",
        alignItems: "start",
        gap: "var(--space-md)",
      }}
    >
      <FormField
        label="Who hears the anchor alarm"
        error={chosen === undefined ? "Choose who hears the alarm, so a drag at night wakes someone." : undefined}
      >
        <Select.Root
          value={contact}
          onValueChange={(next: string | null): void => {
            if (next !== null) setContact(next);
          }}
        >
          <Select.Trigger aria-label="Who hears the anchor alarm" aria-invalid={chosen === undefined}>
            <Select.Value placeholder="Choose a crew member">{chosen?.name}</Select.Value>
          </Select.Trigger>
          <Select.Portal>
            <Select.Positioner>
              <Select.Popup>
                <Select.List>
                  <Select.Group>
                    <Select.GroupLabel>On watch</Select.GroupLabel>
                    {ON_WATCH.map(contactItem)}
                  </Select.Group>
                  <Select.Separator />
                  <Select.Group>
                    <Select.GroupLabel>Off watch</Select.GroupLabel>
                    {OFF_WATCH.map(contactItem)}
                  </Select.Group>
                  <Select.Separator />
                  <Select.Group>
                    <Select.GroupLabel>Ashore</Select.GroupLabel>
                    {SHORE.map(contactItem)}
                  </Select.Group>
                </Select.List>
              </Select.Popup>
            </Select.Positioner>
          </Select.Portal>
        </Select.Root>
      </FormField>

      <FormField label="True wind">
        <div style={ROW}>
          <Wind size={16} aria-hidden="true" />
          <Reading
            value={wind}
            unit={unit}
            label="TWS"
            source="Masthead"
            fractionDigits={unit === "km/h" ? 0 : 1}
            maxDigits={2}
          />
          <Select.Root
            value={unit}
            onValueChange={(value: string | null): void => {
              if (value !== null) setUnit(value as WindUnit);
            }}
          >
            <Select.Trigger aria-label="Wind speed unit" style={{ width: 88 }}>
              <Select.Value />
            </Select.Trigger>
            <Select.Portal>
              <Select.Positioner side="bottom" align="end" sideOffset={4} alignItemWithTrigger={false}>
                <Select.Popup>
                  <Select.List>
                    <Select.Item value="kn">kn</Select.Item>
                    <Select.Item value="m/s">m/s</Select.Item>
                    <Select.Item value="km/h">km/h</Select.Item>
                  </Select.List>
                </Select.Popup>
              </Select.Positioner>
            </Select.Portal>
          </Select.Root>
        </div>
      </FormField>
    </div>
  );
}

A select is a list of choices that opens from a button. The trigger shows the chosen item's text with a chevron after it. The open list renders in the page body through a portal and is aligned over the trigger. Each part is its named part of Select, built on Base UI. Props and cancellable change events follow the corresponding Base UI part. The trigger is as tall as an input: 48 px, the touch target, on a phone or a tablet (a screen up to the 68.75em breakpoint) or under a coarse pointer, and 44 px on a desktop with a fine pointer. On a coarse pointer, every choice in the open list also has at least the shared touch-target height. The list uses pointer capability because its portal sits outside the trigger's screen container.

Examples

Closed

The trigger shows the chosen item; a tap opens the list over it.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string = "The trigger shows the chosen item; a tap opens the list over it.";

export default function Closed(): ReactElement {
  return (
    <Select.Root defaultValue="en">
      <Select.Trigger aria-label="Language">
        <Select.Value />
      </Select.Trigger>
      <Select.Portal>
        <Select.Positioner>
          <Select.Popup>
            <Select.List>
              <Select.Group>
                <Select.GroupLabel>Languages</Select.GroupLabel>
                <Select.Item value="en">English</Select.Item>
                <Select.Item value="nl">Nederlands</Select.Item>
                <Select.Separator />
                <Select.Item value="de">Deutsch</Select.Item>
              </Select.Group>
            </Select.List>
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  );
}

Press the trigger to open the list. The trigger shows the chosen item's text, and the list opens over it with the chosen item in place.

Choose a select when there are more than about five options or the space is tight. For five or fewer options that the crew compare, a segmented control or a radio group shows every option at once and saves a tap. When the crew need to search a long list by typing, use a combobox.

Placeholder

With no value chosen the trigger shows a placeholder in the dim text color; the placeholder is a prompt, not an item.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "With no value chosen the trigger shows a placeholder in the dim text color; the placeholder is a prompt, not an item.";

export default function Placeholder(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root>
        <Select.Trigger aria-label="Anchorage">
          <Select.Value placeholder="Choose an anchorage" />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          <Select.Item value="porto-pollensa">Porto Pollensa</Select.Item>
          <Select.Item value="cala-portals">Cala Portals</Select.Item>
          <Select.Item value="sant-elm">Sant Elm</Select.Item>
          <Select.Item value="puerto-andratx">Puerto de Andratx</Select.Item>
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

Leave value unset and give Select.Value a placeholder when no item is chosen yet. The placeholder prompts the crew ("Choose an anchorage"); it is not an item and cannot be chosen. Start with a chosen item instead when one choice is the right default, such as the current language.

Groups

A sail plan in three families: each group has a heading, and a separator marks where one family ends and the next begins.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "A sail plan in three families: each group has a heading, and a separator marks where one family ends and the next begins.";

export default function Groups(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root defaultValue="genoa">
        <Select.Trigger aria-label="Sail">
          <Select.Value />
        </Select.Trigger>
        <Select.Portal>
          <Select.Positioner>
            <Select.Popup>
              <Select.List>
                <Select.Group>
                  <Select.GroupLabel>Headsails</Select.GroupLabel>
                  <Select.Item value="genoa">Genoa</Select.Item>
                  <Select.Item value="working-jib">Working jib</Select.Item>
                  <Select.Item value="storm-jib">Storm jib</Select.Item>
                </Select.Group>
                <Select.Separator />
                <Select.Group>
                  <Select.GroupLabel>Mainsail</Select.GroupLabel>
                  <Select.Item value="full-main">Full main</Select.Item>
                  <Select.Item value="first-reef">First reef</Select.Item>
                  <Select.Item value="second-reef">Second reef</Select.Item>
                </Select.Group>
                <Select.Separator />
                <Select.Group>
                  <Select.GroupLabel>Downwind</Select.GroupLabel>
                  <Select.Item value="spinnaker">Spinnaker</Select.Item>
                  <Select.Item value="code-zero">Code zero</Select.Item>
                </Select.Group>
              </Select.List>
            </Select.Popup>
          </Select.Positioner>
        </Select.Portal>
      </Select.Root>
    </div>
  );
}

Group the list when its items fall into families the crew already know. Wrap each family in Select.Group with a Select.GroupLabel for its heading, and put a Select.Separator between groups. A group of one or two items, or a list with no natural families, reads better flat.

Equipment by system

An equipment picker grouped by system, so the crew find a pump by where it is plumbed rather than by its name.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "An equipment picker grouped by system, so the crew find a pump by where it is plumbed rather than by its name.";

export default function EquipmentBySystem(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root>
        <Select.Trigger aria-label="Equipment">
          <Select.Value placeholder="Choose equipment" />
        </Select.Trigger>
        <Select.Portal>
          <Select.Positioner>
            <Select.Popup>
              <Select.List>
                <Select.Group>
                  <Select.GroupLabel>Engine room</Select.GroupLabel>
                  <Select.Item value="main-engine">Main engine</Select.Item>
                  <Select.Item value="fuel-filter">Fuel filter</Select.Item>
                  <Select.Item value="raw-water-pump">Raw water pump</Select.Item>
                </Select.Group>
                <Select.Separator />
                <Select.Group>
                  <Select.GroupLabel>Bilge</Select.GroupLabel>
                  <Select.Item value="forward-bilge-pump">Forward bilge pump</Select.Item>
                  <Select.Item value="aft-bilge-pump">Aft bilge pump</Select.Item>
                </Select.Group>
                <Select.Separator />
                <Select.Group>
                  <Select.GroupLabel>Electrical</Select.GroupLabel>
                  <Select.Item value="house-bank">House battery bank</Select.Item>
                  <Select.Item value="start-battery">Start battery</Select.Item>
                  <Select.Item value="shore-charger">Shore charger</Select.Item>
                </Select.Group>
              </Select.List>
            </Select.Popup>
          </Select.Positioner>
        </Select.Portal>
      </Select.Root>
    </div>
  );
}

A grouped picker with a placeholder: the crew look for the equipment by the system it belongs to, then pick within it. Keep the order of groups the same everywhere the list appears.

Controlled

Value held by the parent: first-reef
The parent holds the chosen value and passes it down; the value it keeps is the item's value, not the text shown.
Code
import { Select } from "@bosunkit/components/select";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The parent holds the chosen value and passes it down; the value it keeps is the item's value, not the text shown.";

export default function Controlled(): ReactElement {
  const [sail, setSail]: [string, Dispatch<SetStateAction<string>>] = useState<string>("first-reef");
  return (
    <div style={{ display: "flex", alignItems: "center", gap: "var(--space-md)", flexWrap: "wrap" }}>
      <div style={{ width: 220 }}>
        <Select.Root value={sail} onValueChange={(next: string | null): void => { (setSail)(next ?? ""); }}>
          <Select.Trigger aria-label="Mainsail">
            <Select.Value />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
            <Select.Item value="full-main">Full main</Select.Item>
            <Select.Item value="first-reef">First reef</Select.Item>
            <Select.Item value="second-reef">Second reef</Select.Item>
            <Select.Item value="third-reef">Third reef</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </div>
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--text-dim)" }}>
        Value held by the parent: <span style={{ color: "var(--text)" }}>{sail}</span>
      </span>
    </div>
  );
}

The parent holds the value and passes value and onValueChange. The select reports the chosen item's value, so keep the text the crew read in the item and an identifier in the value.

Disabled items

A disabled item is dimmed and cannot be chosen; its text says why it is out.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "A disabled item is dimmed and cannot be chosen; its text says why it is out.";

export default function DisabledItems(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root defaultValue="working-jib">
        <Select.Trigger aria-label="Headsail">
          <Select.Value />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          <Select.Item value="genoa" disabled>
            Genoa (in for repair)
          </Select.Item>
          <Select.Item value="working-jib">Working jib</Select.Item>
          <Select.Item value="storm-jib">Storm jib</Select.Item>
          <Select.Item value="code-zero" disabled>
            Code zero (not on board)
          </Select.Item>
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

Disable an item the crew can see but cannot use now, and say why in its text. A disabled item stays in the list so the crew do not wonder where it went. Remove an item that can never apply.

Disabled

Set when the vessel was commissioned. Ask the owner to change it.
A disabled select is dimmed and skipped by the keyboard; the hint under it says why it cannot be changed.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "A disabled select is dimmed and skipped by the keyboard; the hint under it says why it cannot be changed.";

export default function Disabled(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root disabled defaultValue="saildrive">
        <Select.Trigger aria-label="Propulsion" aria-describedby="propulsion-hint">
          <Select.Value />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          <Select.Item value="saildrive">Saildrive</Select.Item>
          <Select.Item value="shaft">Shaft</Select.Item>
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
      <div
        id="propulsion-hint"
        style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text-dim)", marginTop: 4 }}
      >
        Set when the vessel was commissioned. Ask the owner to change it.
      </div>
    </div>
  );
}

A disabled select cannot be opened. Say why under it and name the way to change it, with aria-describedby pointing at the hint.

With detail

Each anchorage carries its distance after the name; the trigger shows the name alone, through the value's own children.
Code
import { Select } from "@bosunkit/components/select";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Each anchorage carries its distance after the name; the trigger shows the name alone, through the value's own children.";

interface Anchorage {
  readonly value: string;
  readonly name: string;
  readonly distance: string;
}

const ANCHORAGES: readonly Anchorage[] = [
  { value: "cala-portals", name: "Cala Portals", distance: "2.1 NM" },
  { value: "sant-elm", name: "Sant Elm", distance: "6.8 NM" },
  { value: "porto-pollensa", name: "Porto Pollensa", distance: "18.4 NM" },
  { value: "puerto-andratx", name: "Puerto de Andratx", distance: "21.0 NM" },
];

export default function WithDetail(): ReactElement {
  const [chosen, setChosen]: [string, Dispatch<SetStateAction<string>>] = useState<string>("cala-portals");
  const name: string = ANCHORAGES.find((anchorage: Anchorage): boolean => anchorage.value === chosen)?.name ?? "";
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root value={chosen} onValueChange={(next: string | null): void => { (setChosen)(next ?? ""); }}>
        <Select.Trigger aria-label="Anchorage">
          <Select.Value>{name}</Select.Value>
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          {ANCHORAGES.map(
            (anchorage: Anchorage): ReactElement => (
              <Select.Item key={anchorage.value} value={anchorage.value}>
                {anchorage.name}
                <span style={{ marginInlineStart: "var(--space-sm)", color: "var(--text-dim)", fontSize: 11 }}>
                  {anchorage.distance}
                </span>
              </Select.Item>
            ),
          )}
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

An item can hold more than text. Put a secondary detail after the name in a dimmed span, such as an anchorage's distance. The trigger repeats whatever the chosen item holds; pass the name as the children of Select.Value to show the name alone.

With icons

An icon before each watch station; the trigger repeats the chosen item's icon and text, so the choice reads at a glance.
Code
import { Select } from "@bosunkit/components/select";
import { Compass, Eye, Map as MapIcon, Radio, type LucideIcon } from "lucide-react";
import type { CSSProperties, ReactElement } from "react";

export const description: string =
  "An icon before each watch station; the trigger repeats the chosen item's icon and text, so the choice reads at a glance.";

interface Station {
  readonly value: string;
  readonly name: string;
  readonly icon: LucideIcon;
}

const STATIONS: readonly Station[] = [
  { value: "helm", name: "Helm", icon: Compass },
  { value: "lookout", name: "Lookout", icon: Eye },
  { value: "navigator", name: "Navigator", icon: MapIcon },
  { value: "radio", name: "Radio", icon: Radio },
];

const ROW: CSSProperties = { display: "inline-flex", alignItems: "center", gap: "var(--space-sm)" };

export default function WithIcons(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root defaultValue="lookout">
        <Select.Trigger aria-label="Watch station">
          <Select.Value />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          {STATIONS.map(
            (station: Station): ReactElement => (
              <Select.Item key={station.value} value={station.value}>
                <span style={ROW}>
                  <station.icon size={14} aria-hidden="true" />
                  {station.name}
                </span>
              </Select.Item>
            ),
          )}
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

An icon before the text helps the crew find a familiar item. Mark the icon decorative with aria-hidden, since the text names the item. Use an icon on every item or on none.

Crew roles

A crew member with their role after the name, grouped by who is on watch; the trigger shows the name alone.
Code
import { Select } from "@bosunkit/components/select";
import { useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A crew member with their role after the name, grouped by who is on watch; the trigger shows the name alone.";

interface CrewMember {
  readonly value: string;
  readonly name: string;
  readonly role: string;
}

const ON_WATCH: readonly CrewMember[] = [
  { value: "anneke", name: "Anneke", role: "Skipper" },
  { value: "joost", name: "Joost", role: "Navigator" },
];

const OFF_WATCH: readonly CrewMember[] = [
  { value: "femke", name: "Femke", role: "Engineer" },
  { value: "daan", name: "Daan", role: "Deckhand" },
];

const DETAIL_STYLE: CSSProperties = { marginInlineStart: "var(--space-sm)", color: "var(--text-dim)", fontSize: 11 };

function crewItem(member: CrewMember): ReactElement {
  return (
    <Select.Item key={member.value} value={member.value}>
      {member.name}
      <span style={DETAIL_STYLE}>{member.role}</span>
    </Select.Item>
  );
}

export default function CrewRoles(): ReactElement {
  const [chosen, setChosen]: [string, Dispatch<SetStateAction<string>>] = useState<string>("joost");
  const name: string = [...ON_WATCH, ...OFF_WATCH].find((member: CrewMember): boolean => member.value === chosen)?.name ?? "";
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root
        value={chosen}
        onValueChange={(next: string | null): void => {
          if (next !== null) setChosen(next);
        }}
      >
        <Select.Trigger aria-label="Crew member">
          <Select.Value>{name}</Select.Value>
        </Select.Trigger>
        <Select.Portal>
          <Select.Positioner>
            <Select.Popup>
              <Select.List>
                <Select.Group>
                  <Select.GroupLabel>On watch</Select.GroupLabel>
                  {ON_WATCH.map(crewItem)}
                </Select.Group>
                <Select.Group>
                  <Select.GroupLabel>Off watch</Select.GroupLabel>
                  {OFF_WATCH.map(crewItem)}
                </Select.Group>
              </Select.List>
            </Select.Popup>
          </Select.Positioner>
        </Select.Portal>
      </Select.Root>
    </div>
  );
}

Groups and secondary detail together: each crew member with their role, under who is on watch. The trigger shows the name alone.

Long list

Thirty harbors in one list: the list is as tall as the room above and below the trigger, and scrolls past that.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "Thirty harbors in one list: the list is as tall as the room above and below the trigger, and scrolls past that.";

const HARBORS: readonly string[] = [
  "Alicante",
  "Antibes",
  "Barcelona",
  "Bonifacio",
  "Cagliari",
  "Calvi",
  "Cannes",
  "Cartagena",
  "Ciutadella",
  "Dénia",
  "Formentera",
  "Genoa",
  "Ibiza",
  "La Spezia",
  "Mahón",
  "Marseille",
  "Monaco",
  "Nice",
  "Palma",
  "Palamós",
  "Port-Cros",
  "Porto Cervo",
  "Porto Vecchio",
  "Rosas",
  "Sant Antoni",
  "Sète",
  "Tarragona",
  "Toulon",
  "Valencia",
  "Villefranche",
];

export default function LongList(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root>
        <Select.Trigger aria-label="Destination">
          <Select.Value placeholder="Choose a destination" />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
          {HARBORS.map(
            (harbor: string): ReactElement => (
              <Select.Item key={harbor} value={harbor}>
                {harbor}
              </Select.Item>
            ),
          )}
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

A list taller than the room around the trigger scrolls inside the list. Use a select for up to a few dozen items; a list the crew would scroll for long is better as a combobox.

Positioning

With position popper the list opens beside the trigger instead of over it, and side and align place it.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "With position popper the list opens beside the trigger instead of over it, and side and align place it.";

export default function Popper(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <Select.Root defaultValue="palma">
        <Select.Trigger aria-label="Destination">
          <Select.Value />
        </Select.Trigger>
        <Select.Portal><Select.Positioner alignItemWithTrigger={false} side="bottom" align="start" sideOffset={4}><Select.Popup style={{
            width: "var(--anchor-width)",
            maxHeight: "var(--available-height)",
          }}><Select.List>
          <Select.Item value="palma">Palma</Select.Item>
          <Select.Item value="mahon">Mahón</Select.Item>
          <Select.Item value="ibiza">Ibiza</Select.Item>
          <Select.Item value="formentera">Formentera</Select.Item>
        </Select.List></Select.Popup></Select.Positioner></Select.Portal>
      </Select.Root>
    </div>
  );
}

Select.Positioner aligns the list over the trigger with alignItemWithTrigger. Pass alignItemWithTrigger={false} to open the list beside the trigger instead, and place it with side, align and sideOffset. Base UI's positioner publishes the --anchor-width and --available-height variables, so a popper list can match the trigger's width and stay inside the screen. Use popper where the trigger sits near an edge or must not be covered.

In a form field

A form field names the select; the trigger carries the same name for a screen reader, as an input does.
Code
import { FormField } from "@bosunkit/components/form-field";
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "A form field names the select; the trigger carries the same name for a screen reader, as an input does.";

export default function InFormField(): ReactElement {
  return (
    <div style={{ maxWidth: 320 }}>
      <FormField label="Fuel type">
        <Select.Root defaultValue="diesel">
          <Select.Trigger aria-label="Fuel type">
            <Select.Value />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
            <Select.Item value="diesel">Diesel</Select.Item>
            <Select.Item value="petrol">Petrol</Select.Item>
            <Select.Item value="lpg">LPG</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </FormField>
    </div>
  );
}

A form field puts the label above the select. The trigger needs its own accessible name, the same as the label, as an input does.

With an error

With nothing chosen the trigger is marked invalid and the field says what to do; choosing an item clears the error.
Code
import { FormField } from "@bosunkit/components/form-field";
import { Select } from "@bosunkit/components/select";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "With nothing chosen the trigger is marked invalid and the field says what to do; choosing an item clears the error.";

export default function WithError(): ReactElement {
  const [harbor, setHarbor]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  const invalid: boolean = harbor === "";
  return (
    <div style={{ maxWidth: 320 }}>
      <FormField label="Departure harbor" error={invalid ? "Choose the harbor you are leaving from." : undefined}>
        <Select.Root value={harbor} onValueChange={(next: string | null): void => { (setHarbor)(next ?? ""); }}>
          <Select.Trigger aria-label="Departure harbor" aria-invalid={invalid}>
            <Select.Value placeholder="Choose a harbor" />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
            <Select.Item value="palma">Palma</Select.Item>
            <Select.Item value="mahon">Mahón</Select.Item>
            <Select.Item value="ibiza">Ibiza</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </FormField>
    </div>
  );
}

Set aria-invalid on the trigger and pass the field's error. The error says what to do ("Choose the harbor you are leaving from"), and clears when an item is chosen. The trigger uses a dashed neutral border; its keyboard focus ring remains visible around that border.

Units

A compact select after a reading chooses its unit; the reading's value and unit change with it.
Code
import { Reading } from "@bosunkit/components/reading";
import { ReadoutSize } from "@bosunkit/instruments";
import { Select } from "@bosunkit/components/select";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A compact select after a reading chooses its unit; the reading's value and unit change with it.";

const SPEED_THROUGH_WATER: Readonly<Record<string, number>> = {
  kn: 6.4,
  "m/s": 3.3,
  "km/h": 11.9,
};

export default function Units(): ReactElement {
  const [unit, setUnit]: [string, Dispatch<SetStateAction<string>>] = useState<string>("kn");
  return (
    <div style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
      <Reading
        value={SPEED_THROUGH_WATER[unit]}
        unit={unit}
        label="STW"
        source="Log"
        fractionDigits={1}
        maxDigits={2}
        size={ReadoutSize.large}
      />
      <div style={{ width: 112 }}>
        <Select.Root value={unit} onValueChange={(next: string | null): void => { (setUnit)(next ?? ""); }}>
          <Select.Trigger aria-label="Speed unit">
            <Select.Value />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={false} sideOffset={4}><Select.Popup ><Select.List>
            <Select.Item value="kn">kn</Select.Item>
            <Select.Item value="m/s">m/s</Select.Item>
            <Select.Item value="km/h">km/h</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </div>
    </div>
  );
}

A compact select after a reading chooses its unit. Fix the select's width so the reading does not move when the unit changes, and write the unit in the item exactly as it appears beside the reading (kn, m/s, km/h).

In a settings row

Chart orientation
Which way is up on the chart. Course up turns the chart with the vessel.
A setting row: the name and what it does on the left, the select on the right, with the select named by the row's own text.
Code
import { Select } from "@bosunkit/components/select";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A setting row: the name and what it does on the left, the select on the right, with the select named by the row's own text.";

export default function SettingsRow(): ReactElement {
  const [orientation, setOrientation]: [string, Dispatch<SetStateAction<string>>] = useState<string>("north-up");
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        gap: "var(--space-md)",
        flexWrap: "wrap",
        padding: "var(--space-md)",
        border: "1px solid var(--border)",
        borderRadius: "var(--radius-md)",
      }}
    >
      <div style={{ fontFamily: "var(--font-mono)", maxWidth: 260 }}>
        <div id="chart-orientation-label" style={{ fontSize: 13 }}>
          Chart orientation
        </div>
        <div style={{ fontSize: 11, color: "var(--text-dim)", marginTop: 2 }}>
          Which way is up on the chart. Course up turns the chart with the vessel.
        </div>
      </div>
      <div style={{ width: 160 }}>
        <Select.Root value={orientation} onValueChange={(next: string | null): void => { (setOrientation)(next ?? ""); }}>
          <Select.Trigger aria-labelledby="chart-orientation-label">
            <Select.Value />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={false} align="end" sideOffset={4}><Select.Popup ><Select.List>
            <Select.Item value="north-up">North up</Select.Item>
            <Select.Item value="course-up">Course up</Select.Item>
            <Select.Item value="head-up">Head up</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </div>
    </div>
  );
}

In a settings row the name and the description are the select's label: point aria-labelledby at the name.

Motion

The list fades in over the medium motion duration when it opens, and leaves at once when a choice closes it. It fades and never moves or grows, because the select places the list by measuring it, and a list caught mid-animation would be placed by the wrong size. motion={false} on Select.Popup opens it at once.

Two selects side by side: the first list fades in as it opens, the second, with `motion={false}`, opens at once.
Code
import { Select } from "@bosunkit/components/select";
import type { ReactElement } from "react";

export const description: string =
  "Two selects side by side: the first list fades in as it opens, the second, with `motion={false}`, opens at once.";

function Units({ motion, label }: { motion: boolean; label: string }): ReactElement {
  return (
    <Select.Root defaultValue="m">
      <Select.Trigger aria-label={label}>
        <Select.Value />
      </Select.Trigger>
      <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup motion={motion}><Select.List>
        <Select.Item value="m">Meters</Select.Item>
        <Select.Item value="ft">Feet</Select.Item>
        <Select.Item value="fa">Fathoms</Select.Item>
      </Select.List></Select.Popup></Select.Positioner></Select.Portal>
    </Select.Root>
  );
}

export default function Motion(): ReactElement {
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-md)" }}>
      <Units motion label="Depth unit, with motion" />
      <Units motion={false} label="Depth unit, without motion" />
    </div>
  );
}

Props

Select.Root owns value and visibility. Use Select.Label to name its trigger and Select.GroupLabel for a group heading. Supply items for labels available during server rendering or when custom child components hide item declarations. Direct Select.Item children are also discovered for the initial selected label.

PropTypeDefaultDescription
childrenReact.ReactNode–
inputRefReact.Ref<HTMLInputElement> | undefined–A ref to access the hidden input element.
namestring | undefined–Identifies the field when a form is submitted.
formstring | undefined–Identifies the form that owns the hidden input. Useful when the select is rendered outside the form.
autoCompletestring | undefined–Provides a hint to the browser for autofill.
idstring | undefined–The id of the Select.
requiredboolean | undefined–Whether the user must choose a value before submitting a form.
readOnlyboolean | undefined–Whether the user should be unable to choose a different option from the select popup.
disabledboolean | undefined–Whether the component should ignore user interaction.
multipleMultiple | undefined–Whether multiple items can be selected.
highlightItemOnHoverboolean | undefined–Whether moving the pointer over items should highlight them. Disabling this prop allows CSS :hover to be differentiated from the :focus (data-highlighted) state.
defaultOpenboolean | undefined–Whether the select popup is initially open. To render a controlled select popup, use the open prop instead.
onOpenChange((open: boolean, eventDetails: SelectRootOpenChangeEventDetails) => void) | undefined–Event handler called when the select popup is opened or closed.
onOpenChangeComplete((open: boolean) => void) | undefined–Event handler called after any animations complete when the select popup is opened or closed.
openboolean | undefined–Whether the select popup is currently open.
modalboolean | undefined–Determines if the select enters a modal state when open. - true: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled. - false: user interaction with the rest of the document is allowed. On touch devices, a true modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior.
actionsRefReact.RefObject<SelectRootActions | null> | undefined–A ref to imperative actions. - unmount: Ends the closing phase of the select after an externally controlled closing animation finishes. Call preventUnmountOnClose() in onOpenChange first, otherwise the select completes closing on its own. Whether it leaves the DOM is decided by keepMounted on the portal. - close: Closes the select imperatively when called.
itemsRecord<string, React.ReactNode> | ReadonlyArray<{ label: React.ReactNode; value: any; }> | ReadonlyArray<Group<any>> | undefined–Data structure of the items rendered in the select popup. When specified, <Select.Value> renders the label of the selected item instead of the raw value.
itemToStringLabel((itemValue: Value) => string) | undefined–When the item values are objects (<Select.Item value={object}>), this function converts the object value to a string representation for display in the trigger. If the shape of the object is { value, label }, the label will be used automatically without needing to specify this prop.
itemToStringValue((itemValue: Value) => string) | undefined–When the item values are objects (<Select.Item value={object}>), this function converts the object value to a string representation for form submission. If the shape of the object is { value, label }, the value will be used automatically without needing to specify this prop.
isItemEqualToValue((itemValue: Value, value: Value) => boolean) | undefined–Custom comparison logic used to determine if a select item value matches the current selected value. Useful when item values are objects without matching referentially. Defaults to Object.is comparison.
defaultValueSelectInputValue<Value, Multiple> | null | undefined–The uncontrolled value of the select when it's initially rendered. To render a controlled select, use the value prop instead.
valueSelectInputValue<Value, Multiple> | null | undefined–The value of the select. Use when controlled.
onValueChange((value: SelectOutputValue<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: SelectRootChangeEventDetails) => void) | undefined–Event handler called when the value of the select changes.
PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
nativeButtonboolean | undefined–Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
childrenReact.ReactNode–
disabledboolean | undefined–Whether the component should ignore user interaction.

Also takes every attribute of Primitive.Trigger.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
childrenReact.ReactNode | ((value: any) => React.ReactNode)–Accepts a function that returns a ReactNode to format the selected value. Treat the value as read-only: in multiple mode it may be a shared frozen array when nothing is selected.
placeholderReact.ReactNode–The placeholder value to display when no value is selected. This is overridden by children if specified, or by a null item's label in items.

Also takes every attribute of Primitive.Value.Props.

PropTypeDefaultDescription
motionboolean | undefined–Enable the token-based fade.
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
childrenReact.ReactNode–
finalFocusboolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) | undefined–Determines the element to focus when the select popup is closed. - false: Do not move focus. - true: Move focus based on the default behavior (trigger or previously focused element). - RefObject: Move focus to the ref element. - function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing.

Also takes every attribute of Primitive.Popup.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
anchorElement | null | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | undefined–An element to position the popup against. By default, the popup will be positioned against the trigger.
positionMethod'absolute' | 'fixed' | undefined–Determines which CSS position property to use.
sideSide | undefined–Which side of the anchor element to align the popup against. May automatically change to avoid collisions.
sideOffsetnumber | OffsetFunction | undefined–Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a data object parameter with the following properties: - data.anchor: the dimensions of the anchor element with properties width and height. - data.positioner: the dimensions of the positioner element with properties width and height. - data.side: which side of the anchor element the positioner is aligned against. - data.align: how the positioner is aligned relative to the specified side.
alignAlign | undefined–How to align the popup relative to the specified side.
alignOffsetnumber | OffsetFunction | undefined–Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a data object parameter with the following properties: - data.anchor: the dimensions of the anchor element with properties width and height. - data.positioner: the dimensions of the positioner element with properties width and height. - data.side: which side of the anchor element the positioner is aligned against. - data.align: how the positioner is aligned relative to the specified side.
collisionBoundaryBoundary | undefined–An element or a rectangle that delimits the area that the popup is confined to.
collisionPaddingPadding | undefined–Additional space to maintain from the edge of the collision boundary.
stickyboolean | undefined–Whether to maintain the popup in the viewport after the anchor element was scrolled out of view.
arrowPaddingnumber | undefined–Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup.
disableAnchorTrackingboolean | undefined–Whether to disable the popup from tracking any layout shift of its positioning anchor.
collisionAvoidanceCollisionAvoidance | undefined–Determines how to handle collisions when positioning the popup. side controls overflow on the preferred placement axis (top/bottom or left/right): - 'flip': keep the requested side when it fits; otherwise try the opposite side (top and bottom, or left and right). - 'shift': never change side; keep the requested side and move the popup within the clipping boundary so it stays visible. - 'none': do not correct side-axis overflow. align controls overflow on the alignment axis (start/center/end): - 'flip': keep side, but swap start and end when the requested alignment overflows. - 'shift': keep side and requested alignment, then nudge the popup along the alignment axis to fit. - 'none': do not correct alignment-axis overflow. fallbackAxisSide controls fallback behavior on the perpendicular axis when the preferred axis cannot fit: - 'start': allow perpendicular fallback and try the logical start side first (top before bottom, or left before right in LTR). - 'end': allow perpendicular fallback and try the logical end side first (bottom before top, or right before left in LTR). - 'none': do not fallback to the perpendicular axis. When side is 'shift', explicitly setting align only supports 'shift' or 'none'. If align is omitted, it defaults to 'flip'.
alignItemWithTriggerboolean | undefined–Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space.

Also takes every attribute of Primitive.Positioner.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
nativeButtonboolean | undefined–Whether the component renders a native <button> element when replacing it via the render prop. Set to true if the rendered element is a native button.
childrenReact.ReactNode–
valueany–A unique value that identifies this select item.
disabledboolean | undefined–Whether the component should ignore user interaction.
labelstring | undefined–Specifies the text label to use when the item is matched during keyboard text navigation. Defaults to the item text content if not provided.

Also takes every attribute of Primitive.Item.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.

Also takes every attribute of Primitive.Label.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.

Also takes every attribute of Primitive.Group.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.

Also takes every attribute of Primitive.GroupLabel.Props.

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
orientationOrientation | undefined–The orientation of the separator.

Also takes every attribute of Primitive.Separator.Props.

Do
Use a select for a list longer than the row has room for, such as the languages.
Don't
Don't hide two or three choices behind a select. A segmented control shows them all and takes one press; on and off is a switch.

Compound composition

Compose Select.Portal, Select.Positioner, Select.Popup and Select.List for the option surface. Supply items on Select.Root to render selected labels before the popup mounts. A cleared single selection is null.

PropTypeDefaultDescription
motionboolean | undefined–Enable the token-based fade.
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
childrenReact.ReactNode–
finalFocusboolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) | undefined–Determines the element to focus when the select popup is closed. - false: Do not move focus. - true: Move focus based on the default behavior (trigger or previously focused element). - RefObject: Move focus to the ref element. - function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing.

Also takes every attribute of Primitive.Popup.Props.

  • Accessibility: the target sizes and the accessible name a trigger needs.
  • Button: the button a trigger is drawn like.