Skip to content
EMBOSS
Docs menu

Select

A picker with an embossed trigger and a floating option list; the highlighted option presses into the surface as you traverse.

@emboss/select

Installation

pnpm dlx shadcn@latest add @emboss/select

Usage

example.tsx
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

export function Example() {
  return (
    <Select defaultValue="48">
      <SelectTrigger aria-label="Sample rate">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="44.1">44.1 kHz</SelectItem>
        <SelectItem value="48">48 kHz</SelectItem>
      </SelectContent>
    </Select>
  );
}

API reference

Select

The root; renders no element of its own.

PropTypeDefaultDescription
value / defaultValue / onValueChangestring / string / (value) => voidControlled and uncontrolled selection.
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled and uncontrolled open state.

SelectTrigger / SelectValue

The embossed control and the rendered current value. The chevron icon is built in.

Data attributeDescription
data-popup-openPresent on the trigger while the list is open.

SelectContent / SelectItem / SelectGroup / SelectLabel

The floating list and its options. Items show a trailing check when selected.

PropTypeDefaultDescription
value (SelectItem)stringThe option value.
Data attributeDescription
data-highlightedPresent on the option under the pointer or cursor.

Keyboard

KeysAction
EnterSpaceOpens the list; selects the highlighted option.
ArrowUpArrowDownMoves the highlight through the options.
EscCloses the list.

Source

View source — select.tsx
select.tsx
"use client";

import { Select as BaseSelect } from "@base-ui/react/select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";

/**
 * A picker whose trigger is an embossed control and whose option list
 * floats above the page. The highlighted option momentarily presses into
 * the surface as you traverse the list.
 *
 * @example
 * <Select defaultValue="48k">
 *   <SelectTrigger aria-label="Sample rate">
 *     <SelectValue />
 *   </SelectTrigger>
 *   <SelectContent>
 *     <SelectItem value="44.1k">44.1 kHz</SelectItem>
 *     <SelectItem value="48k">48 kHz</SelectItem>
 *   </SelectContent>
 * </Select>
 */
function Select(props: React.ComponentProps<typeof BaseSelect.Root>) {
  return <BaseSelect.Root {...props} />;
}

function SelectTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof BaseSelect.Trigger>) {
  return (
    <BaseSelect.Trigger
      data-slot="select-trigger"
      className={cn(
        "flex h-(--control-md) w-fit min-w-36 cursor-pointer items-center justify-between gap-2 rounded-md bg-surface-2 px-3 text-sm text-ink shadow-emboss-1 focus-ring transition-[box-shadow] duration-(--duration-release) ease-(--ease-release) select-none hover:shadow-emboss-2 data-disabled:pointer-events-none data-disabled:opacity-50 data-popup-open:shadow-flush",
        className,
      )}
      {...props}
    >
      {children}
      <BaseSelect.Icon className="flex text-ink-faint">
        <ChevronDown className="size-4" aria-hidden />
      </BaseSelect.Icon>
    </BaseSelect.Trigger>
  );
}

function SelectValue(props: React.ComponentProps<typeof BaseSelect.Value>) {
  return <BaseSelect.Value data-slot="select-value" {...props} />;
}

function SelectContent({
  className,
  children,
  sideOffset = 6,
  ...props
}: React.ComponentProps<typeof BaseSelect.Popup> & {
  sideOffset?: number;
}) {
  return (
    <BaseSelect.Portal>
      <BaseSelect.Positioner sideOffset={sideOffset} className="z-50">
        <BaseSelect.Popup
          data-slot="select-content"
          className={cn(
            "max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-y-auto rounded-md bg-surface-2 p-1 text-ink shadow-float-1 transition-[opacity,scale] duration-(--duration-press) ease-(--ease-press) data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
            className,
          )}
          {...props}
        >
          {children}
        </BaseSelect.Popup>
      </BaseSelect.Positioner>
    </BaseSelect.Portal>
  );
}

function SelectItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof BaseSelect.Item>) {
  return (
    <BaseSelect.Item
      data-slot="select-item"
      className={cn(
        "grid cursor-pointer grid-cols-[1fr_auto] items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-well data-highlighted:shadow-deboss-1",
        className,
      )}
      {...props}
    >
      <BaseSelect.ItemText className="col-start-1">
        {children}
      </BaseSelect.ItemText>
      <BaseSelect.ItemIndicator className="col-start-2 flex">
        <Check className="size-3.5" aria-hidden />
      </BaseSelect.ItemIndicator>
    </BaseSelect.Item>
  );
}

function SelectGroup(props: React.ComponentProps<typeof BaseSelect.Group>) {
  return <BaseSelect.Group data-slot="select-group" {...props} />;
}

function SelectLabel({
  className,
  ...props
}: React.ComponentProps<typeof BaseSelect.GroupLabel>) {
  return (
    <BaseSelect.GroupLabel
      data-slot="select-label"
      className={cn(
        "tracking-label px-2 py-1.5 font-mono text-label text-ink-faint uppercase text-engraved",
        className,
      )}
      {...props}
    />
  );
}

export {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectGroup,
  SelectLabel,
};