# Select Field

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

## Installation

```tsx
import { SelectField } from "@makeplane/propel/components/select-field";
```
## Usage

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

```tsx
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.

```tsx
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

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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"` | — |  |
| `required` | `boolean` | `false` | Whether the user must choose a value before submitting a form. Marks the field required and adds a `*` asterisk when a visible `label` is present. |
| `icon` | `ReactNode` | — | Element shown before the value, e.g. `<Icon icon={Server} tint="placeholder" />`. |
| `placeholder` | `ReactNode` | — | Shown in the trigger while nothing is selected — Base UI's `Select.Value` placeholder. |
| `footer` | `ReactNode` | — | Sticky content pinned below the scrolling list, e.g. a `SelectFooter`. |
| `header` | `ReactNode` | — | 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. |
| `description` | `ReactNode` | — | Supporting text shown below the label, above the trigger. Always shown, alongside `hint`/`error`. |
| `error` | `ReactNode` | — | Error text shown below the control. |
| `hint` | `ReactNode` | — | 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. |
| `label` | `string` | — |  |
| `aria-label` | `string` | — |  |
