Propel

Select

Lets a user choose one option from a list in a popup.

Server type
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

export default function BasicDemo() {
  return (
    <Select items={SERVER_TYPES} defaultValue="general">
      <SelectField>
        <SelectLabel size="lg">Server type</SelectLabel>
        <SelectTrigger size="lg" />
      </SelectField>
      <SelectContent>
        <SelectList>
          {SERVER_TYPES.map(({ label, value }) => (
            <SelectItem key={value} value={value} size="lg" label={label} />
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

Installation

import {
  Select,
  SelectContent,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

Usage

import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

export default function BasicDemo() {
  return (
    <Select items={SERVER_TYPES} defaultValue="general">
      <SelectField>
        <SelectLabel size="lg">Server type</SelectLabel>
        <SelectTrigger size="lg" />
      </SelectField>
      <SelectContent>
        <SelectList>
          {SERVER_TYPES.map(({ label, value }) => (
            <SelectItem key={value} value={value} size="lg" label={label} />
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

Examples

Placeholder

Without a defaultValue, the trigger shows its placeholder until an option is picked.

Server type
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

export default function PlaceholderDemo() {
  return (
    <Select items={SERVER_TYPES}>
      <SelectField>
        <SelectLabel size="lg">Server type</SelectLabel>
        <SelectTrigger size="lg" placeholder="Choose a server type" />
      </SelectField>
      <SelectContent>
        <SelectList>
          {SERVER_TYPES.map(({ label, value }) => (
            <SelectItem key={value} value={value} size="lg" label={label} />
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

Multiple

Server types
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

export default function MultipleDemo() {
  return (
    <Select multiple items={SERVER_TYPES} defaultValue={["general", "compute"]}>
      <SelectField>
        <SelectLabel size="lg">Server types</SelectLabel>
        <SelectTrigger size="lg" />
      </SelectField>
      <SelectContent>
        <SelectList>
          {SERVER_TYPES.map(({ label, value }) => (
            <SelectItem key={value} value={value} size="lg" label={label} />
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

Grouped

Region
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectGroup,
  SelectGroupLabel,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";

const REGION_GROUPS = [
  {
    label: "Americas",
    items: [
      { label: "US Central 1", value: "us-central-1" },
      { label: "US East 1", value: "us-east-1" },
    ],
  },
  {
    label: "Europe",
    items: [
      { label: "EU Central 1", value: "eu-central-1" },
      { label: "EU West 1", value: "eu-west-1" },
    ],
  },
];

export default function GroupedDemo() {
  return (
    <Select items={REGION_GROUPS.flatMap((group) => group.items)} defaultValue="us-central-1">
      <SelectField>
        <SelectLabel size="lg">Region</SelectLabel>
        <SelectTrigger size="lg" />
      </SelectField>
      <SelectContent>
        <SelectList>
          {REGION_GROUPS.map((group) => (
            <SelectGroup key={group.label}>
              <SelectGroupLabel>{group.label}</SelectGroupLabel>
              {group.items.map(({ label, value }) => (
                <SelectItem key={value} value={value} size="lg" label={label} />
              ))}
            </SelectGroup>
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

Sizes

size runs lg 32 / xl 36 / 2xl 40 px. Pass the same value to SelectLabel, SelectTrigger, and each SelectItem: the label and the option rows both track the trigger’s rung. The label runs 13px at lg, 14px at xl and 2xl — matching what a field wrapper renders at the same control height, so it sits one step calmer than the value at lg and 2xl and matches it at xl. See Sizing for the full ladder.

lg
xl
2xl
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
  type SelectTriggerSize,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

const SIZES: SelectTriggerSize[] = ["lg", "xl", "2xl"];

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-end gap-3">
      {SIZES.map((size) => (
        <Select key={size} items={SERVER_TYPES} defaultValue="general">
          <SelectField>
            <SelectLabel size={size}>{size}</SelectLabel>
            <SelectTrigger size={size} />
          </SelectField>
          <SelectContent>
            <SelectList>
              {SERVER_TYPES.map(({ label, value }) => (
                <SelectItem key={value} value={value} size="lg" label={label} />
              ))}
            </SelectList>
          </SelectContent>
        </Select>
      ))}
    </div>
  );
}

Variants

SelectTrigger’s secondary variant (default) is the bordered surface; ghost drops the border and fill for a trigger that sits directly on its parent surface.

secondary
ghost
Show code
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
  type SelectTriggerVariant,
} from "@makeplane/propel/components/select";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

const VARIANTS: SelectTriggerVariant[] = ["secondary", "ghost"];

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-end gap-3">
      {VARIANTS.map((variant) => (
        <Select key={variant} items={SERVER_TYPES} defaultValue="general">
          <SelectField>
            <SelectLabel size="lg">{variant}</SelectLabel>
            <SelectTrigger size="lg" variant={variant} />
          </SelectField>
          <SelectContent>
            <SelectList>
              {SERVER_TYPES.map(({ label, value }) => (
                <SelectItem key={value} value={value} size="lg" label={label} />
              ))}
            </SelectList>
          </SelectContent>
        </Select>
      ))}
    </div>
  );
}

With icon

SelectTrigger’s icon prop shows a leading glyph before the value, via the shared Icon slot.

Server type
Show code
import { Icon } from "@makeplane/propel/components/icon";
import {
  Select,
  SelectContent,
  SelectField,
  SelectItem,
  SelectLabel,
  SelectList,
  SelectTrigger,
} from "@makeplane/propel/components/select";
import { Server } from "lucide-react";

const SERVER_TYPES = [
  { label: "General purpose", value: "general" },
  { label: "Compute optimized", value: "compute" },
  { label: "Memory optimized", value: "memory" },
];

export default function WithIconDemo() {
  return (
    <Select items={SERVER_TYPES} defaultValue="general">
      <SelectField>
        <SelectLabel size="lg">Server type</SelectLabel>
        <SelectTrigger size="lg" icon={<Icon icon={Server} tint="placeholder" />} />
      </SelectField>
      <SelectContent>
        <SelectList>
          {SERVER_TYPES.map(({ label, value }) => (
            <SelectItem key={value} value={value} size="lg" label={label} />
          ))}
        </SelectList>
      </SelectContent>
    </Select>
  );
}

API Reference

Select

The select Root — Base UI's context/state provider (renders no element of its own). A behavior-only role, so it lives in `components` (rules 1a, 2); the styled parts live in `elements/select` and are grafted onto Base UI behavior here.

PropTypeDefaultDescription
childrenReactNodeThe select's anatomy — a `SelectTrigger` and the popup of items.

SelectTrigger

The ready-made select trigger: grafts Base UI's `Select.Trigger` behavior onto the styled trigger and bakes its anatomy — an optional leading icon, the selected value (Base UI's `Select.Value` grafted onto `SelectValue`), and the owned chevron indicator. Children are baked, so the prop is omitted.

PropTypeDefaultDescription
size(required)"lg" | "xl" | "2xl"Visual size of the trigger: controls its height, text, and icon size. Required.
variant"secondary" | "ghost""secondary"Figma "Prominence": `secondary` is the bordered surface, `ghost` drops the border and fill.
placeholderReactNodeShown in the trigger while nothing is selected — Base UI's `Select.Value` placeholder.
iconReactNodeElement shown before the value, e.g. `<Icon icon={Server} tint="placeholder" />`.

SelectContent

The select list surface: Base UI portal + positioner + popup grafted onto Propel styling.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>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.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"bottomWhich side of the trigger the list opens toward.
sideOffsetnumber | OffsetFunction4Distance in px between the trigger and the list.
align"center" | "start" | "end"startAlignment of the list relative to the trigger along `side`.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the list along the trigger's edge without changing `align`.
collisionPaddingPadding5Minimum gap in px between the list and the viewport edge it would otherwise touch.
collisionBoundaryBoundaryThe element or rect the list is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat to do when the list would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset.
stickybooleanfalseKeep the list on screen after the trigger scrolls out of view, instead of following it out.
positionMethod"absolute" | "fixed""absolute"Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors, e.g. a trigger inside a sticky header.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullPosition against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one. Geometry only; the trigger keeps its role and interactions.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
alignItemWithTriggerbooleanfalseOverlap the popup onto the trigger so the selected item's text lines up with the trigger's value (native `<select>`-style). Figma's spec opens the list flush below the trigger instead, so this defaults off — enable per-instance for the native-alignment look.

SelectItem

The ready-made select option row: grafts Base UI's `Select.Item` behavior onto the shared listbox row and bakes the selection marker — Base UI's `Select.ItemIndicator` grafted onto the styled `SelectItemIndicator` with a check glyph. The marker is `keepMounted` because the `layout="indicator"` row places children positionally: it must occupy the leading column even while unselected (the styled marker hides its glyph off `data-selected`). The `label` prop becomes the item's text via Base UI's `Select.ItemText`.

PropTypeDefaultDescription
size(required)"md" | "lg" | "xl" | "2xl"Visual size of the row: controls its height and text size. Required.
label(required)stringOption label.