snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
---
title: ButtonGroup
description: Group related buttons with joined borders and shared appearance.
---
import ButtonGroupReference from '/snippets/ui/generated/input/button-group.mdx';
Give a group an accessible name and place related buttons inside it. The group joins adjacent borders and can apply a shared size, variant, and color. Buttons retain their own tab stops.
## State ownership
`ButtonGroup` coordinates appearance, not selection. It has no controlled or uncontrolled selection mode. Each button receives its own action handler and application state; see [Button's state ownership example](/ui/primitives/input/button#state-ownership) for loading behavior.
For a mutually exclusive choice, use [RadioGroup](/ui/primitives/input/radio-group), which owns selection for its radio controls.
## Usage
```tsx
import { Button, ButtonGroup } from 'twenty-ui/primitives/input';
export const RecordActions = () => (
<ButtonGroup aria-label="Record actions" size="sm">
<Button>Edit</Button>
<Button>Duplicate</Button>
</ButtonGroup>
);
```
## Props
<ButtonGroupReference />

View File

@@ -0,0 +1,91 @@
---
title: Button
description: Trigger actions or navigate with styled buttons.
---
import ButtonReference from '/snippets/ui/generated/input/button.mdx';
Use `Button` for an action. Supply its visible label as children, and give icon-only buttons an `aria-label`.
For recurring design defaults, use [MainButton](/ui/components/input/main-button)
or [LightButton](/ui/components/input/light-button) from `twenty-ui/components`.
Both share Button's native props, refs, state, and rendering behavior.
## State ownership
`Button` triggers an action and does not manage a selected value, so it has no controlled or uncontrolled selection mode. Its native `value` prop supplies form data, not selection state.
Your application owns `loading`, `disabled`, and `soon`, which default to `false`. Clicking a button does not change these props or automatically show loading while an asynchronous handler runs. Pass the current application state explicitly.
```tsx
import { useState } from 'react';
import { Button } from 'twenty-ui/primitives/input';
type SaveRecordProps = { onSave: () => Promise<void> };
export const SaveRecord = ({ onSave }: SaveRecordProps) => {
const [loading, setLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const handleSave = async () => {
setLoading(true);
setHasError(false);
try {
await onSave();
} catch {
setHasError(true);
} finally {
setLoading(false);
}
};
return (
<>
<Button loading={loading} onClick={handleSave}>
Save record
</Button>
{hasError && <p role="alert">Unable to save the record. Try again.</p>}
</>
);
};
```
The application starts and clears loading around the save operation. The button prevents activation while loading and becomes available again when the operation finishes, including after an error. `MainButton` and `LightButton` use the same state model.
## Appearance
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { IconPlus } from 'twenty-ui/icon';
export const CreateRecord = () => (
<Button variant="solid" color="accent" size="sm" startIcon={<IconPlus />}>
Create record
</Button>
);
```
Choose `solid`, `outline`, `soft`, or `ghost` independently of the `neutral`, `accent`, `danger`, or `success` color. Sizes are `sm` and `md`; the defaults are `outline`, `neutral`, and `md`. `fullWidth` fills the available width. `startIcon` and `endIcon` accept elements and are decorative. `elevated` adds a shadow. `hotkeys` displays keyboard hints on larger screens; bind the shortcuts in your application.
`disabled`, `loading`, and `soon` prevent activation. Loading keeps the label accessible and preserves the button's width while showing a spinner. Customize the upcoming-feature label with `soonLabel`.
## Links and forms
```tsx
import { Button } from 'twenty-ui/primitives/input';
export const DocumentationLink = () => (
<Button href="https://twenty.com" target="_blank" rel="noreferrer">
Read documentation
</Button>
);
```
`href` renders an anchor with link semantics. The caller can supply its own `render` element or router link. Set `href` and preserve an anchor for navigation; without `href`, preserve a native button. Button derives its semantics without public `nativeButton` or `role` flags. Disabled links cannot activate.
Buttons default to `type="button"`. Set `type="submit"` or `type="reset"` explicitly for forms. Native props, event handlers, refs, and Base UI's `render` prop pass through to the root.
## Props
<ButtonReference />

View File

@@ -0,0 +1,114 @@
---
title: Checkbox
description: Select independent options with checked and indeterminate states.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import CheckboxReference from '/snippets/ui/generated/input/checkbox.mdx';
Use `Checkbox` when each option can be selected independently. For a mutually exclusive choice, use [RadioGroup](/ui/primitives/input/radio-group).
<StoryEmbed
storyId="ui-input-checkbox-checkbox--default"
title="Checkbox example"
height={240}
/>
## Uncontrolled state
Use `defaultChecked` to set the initial checked state. The checkbox owns subsequent changes.
```tsx
import { Checkbox } from 'twenty-ui/primitives/input';
export const EmailPreference = () => (
<label>
<Checkbox name="updates" value="email" defaultChecked />
Receive product updates
</label>
);
```
A checked, named checkbox contributes its value to a form. Set `uncheckedValue` if the form should also submit a value when it is unchecked.
## Controlled state
Pass `checked` and update it with `onCheckedChange` to own the same preference in React state. The callback receives the next boolean and event details.
```tsx
import { useState } from 'react';
import { Checkbox } from 'twenty-ui/primitives/input';
export const EmailPreference = () => {
const [checked, setChecked] = useState(true);
return (
<label>
<Checkbox
name="updates"
value="email"
checked={checked}
onCheckedChange={setChecked}
/>
Receive product updates
</label>
);
};
```
## Indeterminate state
An indeterminate checkbox represents a partial selection. Derive it from the child options independently of the checkbox's checked state.
```tsx
import { useState } from 'react';
import { Checkbox } from 'twenty-ui/primitives/input';
export const ColumnSelection = () => {
const [showEmail, setShowEmail] = useState(true);
const [showPhone, setShowPhone] = useState(false);
return (
<fieldset>
<legend>Visible columns</legend>
<label>
<Checkbox
checked={showEmail && showPhone}
indeterminate={showEmail !== showPhone}
onCheckedChange={(checked) => {
setShowEmail(checked);
setShowPhone(checked);
}}
/>
All columns
</label>
<label>
<Checkbox checked={showEmail} onCheckedChange={setShowEmail} />
Email
</label>
<label>
<Checkbox checked={showPhone} onCheckedChange={setShowPhone} />
Phone
</label>
</fieldset>
);
};
```
## Appearance and interaction
| Prop | Values | Default |
| --------- | ------------------------------ | -------- |
| `size` | `sm`, `md` | `sm` |
| `variant` | `solid`, `outline`, `soft` | `solid` |
| `shape` | `square`, `round` | `square` |
| `color` | `accent`, `success`, `warning` | `accent` |
Tab focuses the checkbox and Space toggles it. Clicking its label also toggles it. Use `disabled` to prevent interaction, or `readOnly` when the value should remain unchanged while the control stays focusable. Keep meaning in the label rather than communicating it through color alone.
## 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.
<CheckboxReference />

View File

@@ -0,0 +1,86 @@
---
title: Field
description: Connect a form control to its label, description, and validation feedback.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import FieldReference from '/snippets/ui/generated/input/field.mdx';
Use `Field` to group one control with its accessible label and helper text. Components such as [Input](/ui/primitives/input/input) integrate with the field directly.
<StoryEmbed
storyId="ui-input-field--default"
title="Field example"
height={240}
/>
## Anatomy
Place one control and its supporting parts inside `Field.Root`. This tree shows a field built with the styled `Input` component.
```text
Field.Root
├── Field.Label
├── Input
├── Field.Description
└── Field.Error
```
| Part | Requirement | Purpose |
| ------------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
| `Root` | Required | Shares the name, disabled state, and validation with the control. |
| `Label` | Recommended | Supplies a visible, associated label. An accessible name is always required. |
| `Input` or another integrated control | Required | Receives user input and connects to the field. |
| `Control` | Alternative control | Provides an unstyled input when you do not use an integrated control. |
| `Description` | Optional | Adds persistent helper text associated with the control. |
| `Error` | Optional | Shows feedback for a matching validation state. |
| `Validity` | Optional | Exposes validity through a children render function. |
`Input` and `Textarea` already integrate with the field. Place them directly inside `Root`; they supply the control behavior themselves. Compound controls such as [Select](/ui/primitives/input/select#compose-with-a-field) keep their own root and parts inside the field.
## Labeled control
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => (
<Field.Root name="email" validationMode="onBlur">
<Field.Label>Work email</Field.Label>
<Input type="email" required placeholder="you@example.com" />
<Field.Description>
We send account updates to this address.
</Field.Description>
<Field.Error match="valueMissing">Enter your email address.</Field.Error>
<Field.Error match="typeMismatch">Enter a valid email address.</Field.Error>
</Field.Root>
);
```
`Field.Root` manages the control's name, disabled state, and validity. `Field.Label` and `Field.Description` connect their content to the control. Set validation timing with `validationMode`; use `validate` for custom validation.
## External errors
Use `invalid` when your application owns the error state. An explicit `match` makes the error message visible.
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
type UsernameFieldProps = { error?: string };
export const UsernameField = ({ error }: UsernameFieldProps) => (
<Field.Root name="username" invalid={Boolean(error)}>
<Field.Label>Username</Field.Label>
<Input autoComplete="username" />
<Field.Error match={Boolean(error)}>{error}</Field.Error>
</Field.Root>
);
```
Keep error messages actionable and provide a visible label even when the control has a placeholder.
## 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.
<FieldReference />

View File

@@ -0,0 +1,99 @@
---
title: InputGroup
description: Add prefixes, suffixes, and actions around a text input.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import InputGroupReference from '/snippets/ui/generated/input/input-group.mdx';
`InputGroup` gives an [Input](/ui/primitives/input/input) a shared border and optional leading or trailing content. Set `size` on the group to size both the container and its input.
<StoryEmbed
storyId="ui-input-inputgroup--default"
title="InputGroup example"
height={240}
/>
## Anatomy
Place the input inside `InputGroup` and keep field labels and messages outside the shared border.
```text
Field.Root
├── Field.Label
├── InputGroup
│ └── Input
└── Field.Description
```
| Part or slot | Requirement | Purpose |
| --------------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| `InputGroup` | Required | Supplies the shared border, layout, and size. |
| `Input` as `children` | Required | Owns the input value, native attributes, and editing behavior. |
| `startElement` | Optional prop | Adds a prefix, icon, or other content before the input. |
| `endElement` | Optional prop | Adds a suffix or action after the input. |
| `Field.Root`, `Label`, `Description`, `Error` | Optional composition | Associates a label, helper text, and validation feedback with the input. |
`startElement` and `endElement` are React node props, not separate components. `InputGroup` does not supply a label or manage validation. Give the input an accessible name, either through `Field.Label` or its own labeling attributes.
## Prefix and suffix
```tsx
import { Field, Input, InputGroup } from 'twenty-ui/primitives/input';
export const WebsiteField = () => (
<Field.Root name="website">
<Field.Label>Website</Field.Label>
<InputGroup startElement={<span aria-hidden>https://</span>}>
<Input placeholder="example.com" />
</InputGroup>
<Field.Description>Enter the domain without https://.</Field.Description>
</Field.Root>
);
```
`startElement` and `endElement` accept React nodes. Prefixes and suffixes are visual content; the input's submitted value contains only what the user enters.
## Trailing action
```tsx
import { useState } from 'react';
import { Button, Field, Input, InputGroup } from 'twenty-ui/primitives/input';
export const PasswordField = () => {
const [visible, setVisible] = useState(false);
return (
<Field.Root name="password">
<Field.Label>Password</Field.Label>
<InputGroup
endElement={
<Button
type="button"
aria-label={visible ? 'Hide password' : 'Show password'}
variant="ghost"
size="sm"
onClick={() => setVisible(!visible)}
>
{visible ? 'Hide' : 'Show'}
</Button>
}
>
<Input
type={visible ? 'text' : 'password'}
autoComplete="current-password"
/>
</InputGroup>
</Field.Root>
);
};
```
Put labels, validation, and value handling on the field or input. Give adornment buttons their own accessible names and use `type="button"` when they should not submit the form. The group supports `sm` and `md` sizes and defaults to `md`.
## 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.
<InputGroupReference />

View File

@@ -0,0 +1,85 @@
---
title: Input
description: A text input with native attributes, field integration, and two sizes.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import InputReference from '/snippets/ui/generated/input/input.mdx';
`Input` renders a text input and accepts native input attributes such as `type`, `name`, `required`, `disabled`, and `autoComplete`.
<StoryEmbed
storyId="ui-input-input--default"
title="Input example"
height={240}
/>
## Uncontrolled state
Use `defaultValue` to set the initial email address. The input owns subsequent changes.
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => (
<Field.Root>
<Field.Label>Email</Field.Label>
<Input
name="email"
type="email"
autoComplete="email"
defaultValue="alex@example.com"
/>
<Field.Description>Use your work email address.</Field.Description>
</Field.Root>
);
```
## Controlled state
Pass `value` and update it with `onValueChange` to own the same email address in React state. The callback receives the next string value and event details.
```tsx
import { useState } from 'react';
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => {
const [email, setEmail] = useState('alex@example.com');
return (
<Field.Root>
<Field.Label>Email</Field.Label>
<Input
name="email"
type="email"
autoComplete="email"
value={email}
onValueChange={setEmail}
/>
<Field.Description>Use your work email address.</Field.Description>
</Field.Root>
);
};
```
## Add an adornment
`InputGroup` owns the surrounding border and sizes its input. The default size is `md`; use `sm` for a compact field. Set `size` on the group: a grouped input stretches to the group's height, so its own `size` has no visible effect.
```tsx
import { IconSearch } from 'twenty-ui/icon';
import { Input, InputGroup } from 'twenty-ui/primitives/input';
export const CompactSearch = () => (
<InputGroup size="sm" startElement={<IconSearch size={16} aria-hidden />}>
<Input aria-label="Search contacts" />
</InputGroup>
);
```
## Props
The reference includes component-specific and inherited Base UI props. Native input attributes are also accepted. A standalone input defaults to `md`; inside a group, the group's `size` applies.
<InputReference />

View File

@@ -0,0 +1,117 @@
---
title: RadioGroup
description: Manage a single selection across a set of radio options.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import RadioGroupReference from '/snippets/ui/generated/input/radio-group.mdx';
`RadioGroup` coordinates the value, keyboard navigation, and form submission of its [Radio](/ui/primitives/input/radio) children.
<StoryEmbed
storyId="ui-input-radiogroup--default"
title="RadioGroup example"
height={240}
/>
## Anatomy
Put each `Radio` inside the group and associate it with a label. `RadioGroup` and `Radio` are separate exports.
```text
fieldset
├── legend
└── RadioGroup
├── label
│ └── Radio
└── label
└── Radio
```
| Part | Requirement | Purpose |
| -------------------- | --------------------------- | ---------------------------------------------------------- |
| `RadioGroup` | Required | Owns selection, keyboard navigation, and form integration. |
| `Radio` | One per option | Supplies an option with a distinct `value`. |
| `label` | Recommended for each option | Provides a visible, clickable name for its radio. |
| `fieldset`, `legend` | Optional grouping | Groups related controls under a visible heading. |
Give the group its own accessible name using `aria-label` or `aria-labelledby`. In the examples below, `aria-labelledby` connects the group to the legend. Each radio also needs its own name. Put selection props on `RadioGroup` and option values on `Radio`.
## Uncontrolled state
Use `defaultValue` to set the initial billing frequency. The group owns subsequent changes.
```tsx
import { useId } from 'react';
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const BillingFrequency = () => {
const labelId = useId();
return (
<fieldset>
<legend id={labelId}>Billing frequency</legend>
<RadioGroup
name="billingFrequency"
defaultValue="monthly"
aria-labelledby={labelId}
>
<label>
<Radio value="monthly" />
Monthly
</label>
<label>
<Radio value="yearly" />
Yearly
</label>
</RadioGroup>
</fieldset>
);
};
```
The group's `name` identifies the selected value in form submission. Give the group an accessible name as well as labeling each radio.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same billing frequency in React state. Selection belongs to the group, not to individual radio controls.
```tsx
import { useId, useState } from 'react';
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const BillingFrequency = () => {
const labelId = useId();
const [billingFrequency, setBillingFrequency] = useState('monthly');
return (
<fieldset>
<legend id={labelId}>Billing frequency</legend>
<RadioGroup
name="billingFrequency"
value={billingFrequency}
onValueChange={setBillingFrequency}
aria-labelledby={labelId}
>
<label>
<Radio value="monthly" />
Monthly
</label>
<label>
<Radio value="yearly" />
Yearly
</label>
</RadioGroup>
</fieldset>
);
};
```
`onValueChange` receives the next value and event details. The generic value type is inferred from the props. Use `disabled` on the group to disable every option, or on an individual radio to disable one choice. Arrow keys navigate the enabled options; Tab moves out of the group.
## 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.
<RadioGroupReference />

View File

@@ -0,0 +1,57 @@
---
title: Radio
description: Render one option in a mutually exclusive radio group.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import RadioReference from '/snippets/ui/generated/input/radio.mdx';
`Radio` represents one option inside [RadioGroup](/ui/primitives/input/radio-group). Give each option a distinct `value` and a visible label. The group owns the selected value.
<StoryEmbed
storyId="ui-input-radio--documentation"
title="Radio example"
height={240}
/>
## Labeled options
```tsx
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const ContactMethod = () => (
<RadioGroup
name="contactMethod"
defaultValue="email"
aria-label="Preferred contact method"
>
<label>
<Radio value="email" />
Email
</label>
<label>
<Radio value="phone" />
Phone
</label>
<label>
<Radio value="post" disabled />
Postal mail (unavailable)
</label>
</RadioGroup>
);
```
The `size` prop accepts `sm` or `md` and defaults to `sm`. Use the same size for options in a group.
## Keyboard and selection
Tab enters or leaves the group. Arrow keys move between enabled options and select the focused option. Space selects the focused radio. Selection and change handlers belong on `RadioGroup`; see its [uncontrolled example](/ui/primitives/input/radio-group#uncontrolled-state) and [controlled example](/ui/primitives/input/radio-group#controlled-state).
`Radio` is generic over the value type. Keep that type consistent with its group. Native attributes such as `aria-describedby` can associate an option with additional help text.
## 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.
<RadioReference />

View File

@@ -0,0 +1,240 @@
---
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 />

View File

@@ -0,0 +1,139 @@
---
title: Slider
description: Choose a numeric value or range with draggable, keyboard-accessible thumbs.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SliderReference from '/snippets/ui/generated/input/slider.mdx';
`Slider` supports a single number or an array of numbers for a range. Compose its label, track, indicator, and thumbs inside `Root`.
<StoryEmbed
storyId="ui-input-slider--default"
title="Slider example"
height={240}
/>
## Anatomy
Put the track and thumbs inside `Slider.Control`, which handles pointer interaction. This tree shows a single-thumb slider.
```text
Slider.Root
├── Slider.Label
├── Slider.Value
└── Slider.Control
└── Slider.Track
├── Slider.Indicator
└── Slider.Thumb
```
| Part | Requirement | Purpose |
| ----------- | ----------------------------- | --------------------------------------------------------------- |
| `Root` | Required | Owns the value, bounds, step, and orientation. |
| `Control` | Required | Handles pointer interaction and positions the thumbs. |
| `Track` | Required for the styled track | Shows the full value range. |
| `Thumb` | One per adjustable value | Provides a focusable handle with keyboard interaction. |
| `Label` | Recommended | Supplies a visible label; every thumb needs an accessible name. |
| `Value` | Optional | Displays the current numeric value. |
| `Indicator` | Optional | Shows the selected portion of the track. |
For a range, add a thumb for each value with its matching `index` and a distinct accessible name. Labels and numeric output stay outside `Control`, so they do not become part of the draggable area.
## Uncontrolled state
Use `defaultValue` to set the initial volume. The slider owns subsequent changes.
```tsx
import { Slider } from 'twenty-ui/primitives/input';
export const VolumeSlider = () => (
<Slider.Root name="volume" defaultValue={40} min={0} max={100} step={1}>
<Slider.Label>Volume</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
```
## Controlled state
Pass `value` and update it with `onValueChange` to own the same volume in React state. The callback receives the next value and event details.
```tsx
import { useState } from 'react';
import { Slider } from 'twenty-ui/primitives/input';
export const VolumeSlider = () => {
const [volume, setVolume] = useState(40);
return (
<Slider.Root
name="volume"
value={volume}
onValueChange={setVolume}
min={0}
max={100}
step={1}
>
<Slider.Label>Volume</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
};
```
Use `onValueChange` to keep the controlled value current during an interaction. `onValueCommitted` handles the final value when the interaction finishes, for example to save it. Both callbacks are also available in uncontrolled mode without taking ownership of the value.
## Range
```tsx
import { Slider } from 'twenty-ui/primitives/input';
export const PriceRange = () => (
<Slider.Root
defaultValue={[25, 75]}
min={0}
max={100}
minStepsBetweenValues={10}
>
<Slider.Label>Price range</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb index={0} aria-label="Minimum price" />
<Slider.Thumb index={1} aria-label="Maximum price" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
```
Give range thumbs distinct accessible names and an `index` corresponding to their value. `minStepsBetweenValues` sets the minimum gap in steps. The root's generic value type is a number or a readonly array of numbers.
To control a range, store the array in React state and pass it through `value` and `onValueChange`, just as for a single value.
## Keyboard and appearance
Arrow keys adjust a focused thumb by a step. Home and End move it toward the bounds while respecting range constraints. `orientation="vertical"` changes the layout and keyboard direction. Use `disabled` on `Root` to prevent changes.
Set `color="success"` for the alternative palette; the default is `accent`.
## 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.
<SliderReference />

View File

@@ -0,0 +1,69 @@
---
title: Switch
description: A labeled on or off control with controlled and uncontrolled state.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SwitchReference from '/snippets/ui/generated/input/switch.mdx';
Use `Switch` for an on or off preference. Give the control a visible label or an accessible name.
<StoryEmbed
storyId="ui-input-switch-switch--default"
title="Switch example"
height={240}
/>
## Uncontrolled state
Use `defaultChecked` to set the initial checked state. The switch owns subsequent changes.
```tsx
import { Switch } from 'twenty-ui/primitives/input';
export const NotificationsPreference = () => (
<label>
<Switch name="notifications" defaultChecked />
Email notifications
</label>
);
```
## Controlled state
Pass `checked` and update it with `onCheckedChange` to own the same preference in React state.
```tsx
import { useState } from 'react';
import { Switch } from 'twenty-ui/primitives/input';
export const NotificationsPreference = () => {
const [checked, setChecked] = useState(true);
return (
<label>
<Switch
name="notifications"
checked={checked}
onCheckedChange={setChecked}
/>
Email notifications
</label>
);
};
```
`onCheckedChange` receives the next boolean and event details. The `size` prop accepts `sm` or `md` and defaults to `md`.
## Keyboard and forms
Tab moves focus to the switch; Space toggles it. Clicking its associated label also toggles it. Use `disabled` to prevent interaction or `readOnly` to retain a focusable control whose value cannot be changed.
A named switch submits its value when checked. Use `value` to customize that value, and `uncheckedValue` if the form should also submit a value when it is off.
## Props
The reference includes component-specific and inherited Base UI props. Native attributes such as `aria-label` and `data-*` are also accepted.
<SwitchReference />

View File

@@ -0,0 +1,93 @@
---
title: Textarea
description: Enter multiple lines of text with optional automatic resizing.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TextareaReference from '/snippets/ui/generated/input/textarea.mdx';
Use `Textarea` for notes, descriptions, and other multiline values. It integrates with [Field](/ui/primitives/input/field) for labels and validation.
<StoryEmbed
storyId="ui-input-textarea--default"
title="Textarea example"
height={240}
/>
## Uncontrolled state
Use `defaultValue` to set the initial notes. The textarea owns subsequent changes.
```tsx
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const NotesField = () => (
<Field.Root name="notes">
<Field.Label>Notes</Field.Label>
<Textarea rows={4} defaultValue="Follow up next week." />
<Field.Description>Add context for your team.</Field.Description>
</Field.Root>
);
```
Native textarea attributes such as `rows`, `maxLength`, `required`, and `readOnly` are available. `size` accepts `sm` or `md` and defaults to `md`.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same notes in React state. The callback receives the next string value and event details; native `onChange` is also available.
```tsx
import { useState } from 'react';
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const NotesField = () => {
const [notes, setNotes] = useState('Follow up next week.');
return (
<Field.Root name="notes">
<Field.Label>Notes</Field.Label>
<Textarea rows={4} value={notes} onValueChange={setNotes} />
<Field.Description>Add context for your team.</Field.Description>
</Field.Root>
);
};
```
## Automatic resizing
Automatic resizing works in either state mode. This example also uses the controlled value for a character count.
```tsx
import { useState } from 'react';
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const DescriptionField = () => {
const [description, setDescription] = useState('');
return (
<Field.Root name="description">
<Field.Label>Description</Field.Label>
<Textarea
value={description}
onValueChange={setDescription}
autoResize
rows={2}
maxRows={6}
maxLength={500}
/>
<Field.Description>
{description.length} of 500 characters
</Field.Description>
</Field.Root>
);
};
```
`autoResize` adjusts the textarea's height as its content changes. Use `maxRows` to cap the height. Keep keyboard focus on the textarea while the user types and use helper text for limits.
## 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.
<TextareaReference />