Propel

Select Field

A labeled select with description, popup options, and helper or error text.

Choose the default role for new members.

You can change this later.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const ROLES = [
  { label: "Admin", value: "admin" },
  { label: "Member", value: "member" },
  { label: "Guest", value: "guest" },
];

export default function BasicDemo() {
  return (
    <SelectField
      name="role"
      label="Member role"
      description="Choose the default role for new members."
      size="lg"
      options={ROLES}
      defaultValue="member"
      hint="You can change this later."
    />
  );
}

Installation

import { SelectField } from "@makeplane/propel/components/select-field";

Usage

import { SelectField } from "@makeplane/propel/components/select-field";

const ROLES = [
  { label: "Admin", value: "admin" },
  { label: "Member", value: "member" },
  { label: "Guest", value: "guest" },
];

export default function BasicDemo() {
  return (
    <SelectField
      name="role"
      label="Member role"
      description="Choose the default role for new members."
      size="lg"
      options={ROLES}
      defaultValue="member"
      hint="You can change this later."
    />
  );
}

Examples

Sizes

For the default secondary and ghost variants, size runs md, lg, xl, and 2xl, stepping the label, helper text, and trigger together. The popup’s option rows stay a fixed size regardless, matching the Figma spec.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const VISIBILITY = [
  { label: "Private", value: "private" },
  { label: "Workspace", value: "workspace" },
  { label: "Public", value: "public" },
];

const SIZES = ["lg", "xl", "2xl"] as const;

export default function SizesDemo() {
  return (
    <div className="flex flex-col gap-4">
      {SIZES.map((size) => (
        <SelectField
          key={size}
          name={`visibility-${size}`}
          label={size}
          size={size}
          options={VISIBILITY}
          defaultValue="workspace"
        />
      ))}
    </div>
  );
}

Ghost

Passing variant="ghost" drops the trigger’s border and fill for a select that sits directly on its parent surface.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const VISIBILITY = [
  { label: "Private", value: "private" },
  { label: "Workspace", value: "workspace" },
  { label: "Public", value: "public" },
];

export default function GhostDemo() {
  return (
    <SelectField
      name="visibility-ghost"
      label="Visibility"
      size="lg"
      variant="ghost"
      options={VISIBILITY}
      defaultValue="workspace"
    />
  );
}

Trigger width

width="auto" is the default and preserves the existing 288px minimum. Set width="fit" to make a secondary or ghost trigger hug its icon, value, chevron, gaps, and padding. Popup sizing stays independent, so a longer option can still make the default anchor-sized popup wider than the trigger. Pill remains intrinsically sized and does not accept this prop.

Show code
import { SelectField, type SelectTriggerWidth } from "@makeplane/propel/components/select-field";

const PLANS = [
  { label: "Starter", value: "starter" },
  { label: "A considerably longer enterprise plan", value: "enterprise" },
];

const WIDTHS: SelectTriggerWidth[] = ["auto", "fit"];

export default function FitWidthDemo() {
  return (
    <div className="flex flex-wrap items-end gap-4">
      {WIDTHS.map((width) => (
        <SelectField
          key={width}
          name={`plan-${width}`}
          label={width}
          size="lg"
          width={width}
          options={PLANS}
          defaultValue="starter"
        />
      ))}
    </div>
  );
}

Pill

Passing variant="pill" makes the trigger hug its value for compact property and filter selectors. It follows Propel Pill anatomy and does not add a chevron. SelectField automatically gives its popup content-based sizing so the options remain readable. Pill accepts xs, sm, and md; its surrounding label and helper copy stay on the field’s md typography rung.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const PRIORITIES = [
  { label: "Urgent", value: "urgent" },
  { label: "High", value: "high" },
  { label: "Medium", value: "medium" },
  { label: "Low", value: "low" },
];

export default function PillDemo() {
  return (
    <SelectField
      name="priority-pill"
      label="Priority"
      size="md"
      variant="pill"
      options={PRIORITIES}
      defaultValue="medium"
    />
  );
}

With icon

icon shows a leading glyph before the value, forwarded to the trigger’s Icon slot.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { SelectField } from "@makeplane/propel/components/select-field";
import { ServerOutline } from "@makeplane/propel/icons";

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

export default function WithIconDemo() {
  return (
    <SelectField
      name="serverType"
      label="Server type"
      size="lg"
      options={SERVER_TYPES}
      defaultValue="general"
      icon={<Icon icon={ServerOutline} tint="placeholder" />}
    />
  );
}

Disabled

Contact an admin to change your plan.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const PLANS = [
  { label: "Free", value: "free" },
  { label: "Pro", value: "pro" },
  { label: "Enterprise", value: "enterprise" },
];

export default function DisabledDemo() {
  return (
    <SelectField
      name="plan"
      label="Workspace plan"
      description="Contact an admin to change your plan."
      size="lg"
      options={PLANS}
      defaultValue="pro"
      disabled
    />
  );
}

Invalid

Passing error marks the field invalid and replaces the helper text with the error message.

A project lead is required before publishing.
Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const MEMBERS = [
  { label: "Unassigned", value: "unassigned" },
  { label: "Ana Silva", value: "ana" },
  { label: "Ravi Kumar", value: "ravi" },
];

export default function InvalidDemo() {
  return (
    <SelectField
      name="lead"
      label="Project lead"
      size="lg"
      options={MEMBERS}
      defaultValue="unassigned"
      error="A project lead is required before publishing."
    />
  );
}

Controlled

Selected: medium

Show code
import { SelectField } from "@makeplane/propel/components/select-field";
import * as React from "react";

const PRIORITIES = [
  { label: "Urgent", value: "urgent" },
  { label: "High", value: "high" },
  { label: "Medium", value: "medium" },
  { label: "Low", value: "low" },
];

export default function ControlledDemo() {
  const [value, setValue] = React.useState<string | null>("medium");
  return (
    <SelectField
      name="priority"
      label="Priority"
      description={`Selected: ${value ?? "none"}`}
      size="lg"
      options={PRIORITIES}
      value={value}
      onValueChange={setValue}
    />
  );
}

Multiple

Pass multiple with an array value to select more than one option. The trigger joins the selected option labels, restores the placeholder when the selection is empty, and each option row uses a leading checkbox marker.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

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

export default function MultipleDemo() {
  return (
    <SelectField
      multiple
      name="serverTypes"
      label="Server types"
      size="lg"
      options={SERVER_TYPES}
      defaultValue={["general", "compute"]}
      placeholder="Choose server types"
    />
  );
}

Placeholder

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

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

const ROLES = [
  { label: "Admin", value: "admin" },
  { label: "Member", value: "member" },
  { label: "Guest", value: "guest" },
];

export default function PlaceholderDemo() {
  return (
    <SelectField
      name="role"
      label="Member role"
      size="lg"
      options={ROLES}
      placeholder="Choose a role"
    />
  );
}

Rich options

An option takes any SelectItem prop: icon, description, secondaryText, trailing, disabled, and whatever SelectItem gains later. footer (and header) pin sticky content around the list, and sizing="auto" sizes the popup to its options instead of the trigger.

Show code
import { Badge } from "@makeplane/propel/components/badge";
import { Icon } from "@makeplane/propel/components/icon";
import { SelectFooter } from "@makeplane/propel/components/select";
import { SelectField } from "@makeplane/propel/components/select-field";
import { ModuleOutline, MemoryOutline, ServerOutline } from "@makeplane/propel/icons";

const SERVER_TYPES = [
  {
    label: "General purpose",
    value: "general",
    icon: <Icon icon={ServerOutline} tint="secondary" />,
    description: "Balanced CPU and memory",
  },
  {
    label: "Compute optimized",
    value: "compute",
    icon: <Icon icon={ModuleOutline} tint="secondary" />,
    description: "More vCPUs per GB of memory",
    trailing: <Badge size="xs" variant="brand" label="New" />,
  },
  {
    label: "Memory optimized",
    value: "memory",
    icon: <Icon icon={MemoryOutline} tint="secondary" />,
    secondaryText: "Waitlist",
    disabled: true,
  },
];

export default function RichOptionsDemo() {
  return (
    <SelectField
      name="serverType"
      label="Server type"
      size="lg"
      options={SERVER_TYPES}
      defaultValue="general"
      footer={<SelectFooter>Prices vary by region.</SelectFooter>}
    />
  );
}

Grouped

An entry with its own options is a group: it renders under its label heading, with an optional meta pinned at the heading’s end. Each group also takes a key, which must be unique within the field’s options. Plain options and groups can be mixed. A "separator" entry draws a divider exactly where it sits (nothing is added automatically), so you choose which groups or options get one. Here the Americas and Europe groups share a divider-free block. A group with no options, a separator at either end of the list, or one right after another is skipped.

Show code
import { SelectField, type SelectFieldEntry } from "@makeplane/propel/components/select-field";

const REGIONS: SelectFieldEntry[] = [
  { label: "Nearest region", value: "auto" },
  "separator",
  {
    key: "americas",
    label: "Americas",
    meta: 2,
    options: [
      { label: "US Central 1", value: "us-central-1" },
      { label: "US East 1", value: "us-east-1" },
    ],
  },
  {
    key: "europe",
    label: "Europe",
    meta: 2,
    options: [
      { label: "EU Central 1", value: "eu-central-1" },
      { label: "EU West 1", value: "eu-west-1" },
    ],
  },
  "separator",
  {
    key: "asia-pacific",
    label: "Asia Pacific",
    meta: 1,
    options: [{ label: "AP South 1", value: "ap-south-1" }],
  },
];

export default function GroupedDemo() {
  return (
    <SelectField name="region" label="Region" size="lg" options={REGIONS} defaultValue="auto" />
  );
}

Object values

The field takes its value type (SelectField<Assignee>), so options can carry objects. itemToStringValue turns the selected object into the string the form submits.

Show code
import { SelectField } from "@makeplane/propel/components/select-field";

type Assignee = { id: string; name: string };

const ASSIGNEES: Assignee[] = [
  { id: "u_01", name: "Aaditya Kapoor" },
  { id: "u_02", name: "Bianca Ferreira" },
  { id: "u_03", name: "Marcus Chen" },
];

// Built once, outside render, so the field gets the same `options` array on every render.
const ASSIGNEE_OPTIONS = ASSIGNEES.map((person) => ({ label: person.name, value: person }));

export default function ObjectValuesDemo() {
  return (
    <SelectField<Assignee>
      name="assignee"
      label="Assignee"
      size="lg"
      options={ASSIGNEE_OPTIONS}
      defaultValue={ASSIGNEES[0]}
      itemToStringValue={(person) => person.id}
    />
  );
}

API Reference

SelectField

Ready-to-use select field with label, trigger, popup options, and helper/error text.

PropTypeDefaultDescription
options(required)readonly SelectFieldEntry<Value>[]—Options rendered in the popup. An entry with its own options and no value is a group, rendered under its label heading. A "separator" entry draws a divider exactly where it sits — between groups, between plain options, or both; nothing is added automatically. A group with no options, a separator at either end of the list, or one right after another is skipped, so conditionally built lists never show a bare heading or a stray line. Every option mounts when the popup opens; Select is not windowed. Use ComboboxField for large catalogs, searchable choices or accumulated server pages.
size(required)"xs" | "sm" | "md" | "lg" | "xl" | "2xl"—
requiredbooleanfalseWhether the user must choose a value before submitting a form. Marks the field required and adds a * asterisk when a visible label is present.
iconReactNode—Element shown before the value, e.g. <Icon icon={Server} tint="placeholder" />.
placeholderReactNode—Shown in the trigger while nothing is selected — Base UI's Select.Value placeholder.
footerReactNode—Sticky content pinned below the scrolling list, e.g. a SelectFooter.
headerReactNode—Sticky content pinned above the scrolling list, e.g. a SelectStickyList holding a SelectSelectAll row and a SelectSeparator.
sizing"auto" | "anchor" | "anchor-fixed""anchor"How the popup takes its width. anchor matches the trigger's width, growing only with its content up to the room beside the trigger. auto is the content's width instead — never narrower than 204px, never wider than 400px or the room beside the trigger — for a narrow trigger (e.g. a ghost select in a toolbar) whose options need more room than it has.
descriptionReactNode—Supporting text shown below the label, above the trigger. Always shown, alongside hint/error.
errorReactNode—Error text shown below the control.
hintReactNode—Helper text shown below the control. Replaced by error when an error is set.
variant"secondary" | "ghost" | "pill"—
width"auto" | "fit"—auto keeps the 288px minimum; fit hugs the rendered trigger content.
labelstring—
aria-labelstring—