Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

241 lines
8.3 KiB
Plaintext

---
title: Select
description: Choose one or more values from a popup list of options.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SelectReference from '/snippets/ui/generated/input/select.mdx';
Use `Select` for a form value chosen from a list. Use [Menu](/ui/primitives/surfaces/menu) for commands and actions.
<StoryEmbed
storyId="ui-input-select--documentation"
title="Select example"
height={400}
/>
## Anatomy
Keep the trigger and popup inside the same `Select.Root`. Put option labels directly inside `Select.Item`.
```text
Select.Root
├── Select.Trigger
│ └── Select.Value
└── Select.Popup
└── Select.Item
```
| Part | Requirement | Purpose |
| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------ |
| `Root` | Required | Owns selection, open state, and form integration. |
| `Trigger` | Required for a trigger-based select | Opens the list and displays its current value. |
| `Value` | Optional | Renders the selected label or a placeholder; you can provide custom trigger content. |
| `Popup` | Required for the option list | Renders the positioned, portaled list. |
| `Item` | One per option | Provides selection behavior and an option label. |
| `Group`, `GroupLabel` | Optional | Group related options under a label. |
| `Separator` | Optional | Separates groups visually. |
Twenty UI includes the chevron in `Trigger`, the option text and checkmark in `Item`, and the portal and positioner in `Popup`. Compose these public parts directly; placement and portal options belong on `Popup`.
## Uncontrolled state
Use `defaultValue` to set the initial priority. The select owns subsequent selection changes.
```tsx
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => (
<Select.Root name="priority" defaultValue="normal" items={PRIORITIES}>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
```
Supply `items` on `Root` so `Value` renders the selected item's label; without it, the trigger shows the raw value. The selected value can be `null` when nothing is selected.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same priority in React state. The callback receives the next value and event details.
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => {
const [priority, setPriority] = useState<string | null>('normal');
return (
<Select.Root
name="priority"
value={priority}
onValueChange={setPriority}
items={PRIORITIES}
>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
## Multiple selection
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const LANGUAGES = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'French' },
{ value: 'es', label: 'Spanish' },
];
export const LanguageSelect = () => {
const [languages, setLanguages] = useState<string[]>(['en']);
return (
<Select.Root
multiple
value={languages}
onValueChange={setLanguages}
items={LANGUAGES}
>
<Select.Trigger aria-label="Languages">
<Select.Value placeholder="Choose languages" />
</Select.Trigger>
<Select.Popup>
{LANGUAGES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
With `multiple`, the value is an array. Use `defaultValue={['en']}` instead of `value` and `onValueChange` for an uncontrolled multiple selection. The generic root types represent the option value and whether multiple selection is enabled.
## Open state
Selection and popup visibility are independent. The examples above leave popup visibility uncontrolled. Selecting a single value closes the popup; multiple selection keeps it open. Use `defaultOpen` to set its initial visibility; it defaults to `false`.
Pass `open` and update it with `onOpenChange` to control visibility. This example controls the popup while leaving the selected priority uncontrolled.
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => {
const [open, setOpen] = useState(false);
return (
<Select.Root
name="priority"
defaultValue="normal"
items={PRIORITIES}
open={open}
onOpenChange={setOpen}
>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
## Compose with a field
Wrap the select in `Field.Root` to associate its trigger with a visible label and helper text. The field handles labeling and validation; the select keeps its own selection and popup state.
```tsx
import { Field, Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PriorityField = () => (
<Field.Root name="priority">
<Field.Label>Priority</Field.Label>
<Select.Root name="priority" defaultValue="normal" items={PRIORITIES}>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
<Field.Description>
Choose how soon this record needs attention.
</Field.Description>
</Field.Root>
);
```
## Placement and keyboard
Set `size="sm"` or `size="md"` on `Trigger`. Customize option content with `Item.startIcon`, `Item.endIcon`, and `Item.description` while keeping the option's label in its children.
`Popup` defaults to `side="bottom"`, `align="start"`, and `sideOffset={8}`. Set `alignItemWithTrigger` to align the selected option over the trigger. It uses the [theme's portal container](/ui/theming#scope-an-override) unless you provide `container`.
Keyboard users can open the list from the trigger, navigate options with arrow keys, and type to find an option. Escape dismisses the popup. Label the trigger or integrate the select with [Field](/ui/primitives/input/field); a placeholder alone does not name the control.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<SelectReference />