Select
A field that opens a short, fixed list: pick one value, or clear it back to the placeholder.
- ai-niche-scout · AI startup idea validatorTypeScript
- cennge-ui · React UI libraryTypeScript
- spredo-crypto-screener · Crypto screenerTypeScript
- Open it with a click, Enter, Space or an arrow key; the chosen option carries the check.
- On the closed field, type a letter: the value jumps to the next option that starts with it.
- The × sets Language to null: the placeholder returns and the list shows every repository.
Field marks
- Import
import { Select, SelectItem } from "cennge-ui";- Size
- 4.0 KB gzip for this import, its dependencies aside
- Pattern
- WAI-ARIA select-only combobox (Radix Select)
- Keyboard
- Enter SpaceArrow up Arrow down HomeEndEsc
- State
- Controlled with
valueandonValueChange,nullfor none; or uncontrolled withdefaultValue - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Typeahead
- Letters jump to a matching option, open or closed
- Touch
- A tap on an option never presses what lies under the list
- Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: combobox, dialog-scroll, form, select-touch
- Source
- src/components/select
When to use#
A Select holds one value from a list you know in advance and that fits on a screen: a region, a license, a sort order. Nothing is typed, so a phone opens the list without covering it with a keyboard, and isClearable gives people a way back to "nothing chosen".
Similar#
- Combobox filters as you type and can hold several values; use it when the list is long enough to search.
- Radio shows every choice at once. For a few options that people should compare, it saves a click.
- Dropdown runs actions from a menu; a Select keeps a value.
Pick one, or none#
The value is the chosen item's value, or null when nothing is chosen: never an empty string. isClearable adds a button that sets it back to null.
import { useState } from "react";
import { Select, SelectItem } from "cennge-ui";
export default function LicensePicker() {
const [license, setLicense] = useState<string | null>(null);
return (
<Select
label="License"
placeholder="No license"
value={license}
onValueChange={setLicense}
isClearable
description={license === null ? "value is null" : `value is "${license}"`}
className="w-full max-w-80"
>
<SelectItem value="MIT">MIT</SelectItem>
<SelectItem value="Apache-2.0">Apache 2.0</SelectItem>
<SelectItem value="GPL-3.0">GPL 3.0</SelectItem>
</Select>
);
}Groups and descriptions#
SelectGroup gives a run of options a heading, SelectSeparator draws a line between groups, and an item's description sits under its name. Disabled options stay in the list but cannot be chosen, and the arrow keys pass over them.
import { Select, SelectGroup, SelectItem, SelectSeparator } from "cennge-ui";
export default function RegionPicker() {
return (
<Select label="Region" placeholder="Pick a region" defaultValue="eu-central-1" className="w-full max-w-80">
<SelectGroup label="Europe">
<SelectItem value="eu-central-1" description="Frankfurt">
eu-central-1
</SelectItem>
<SelectItem value="eu-west-1" description="Ireland">
eu-west-1
</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup label="North America">
<SelectItem value="us-east-1" description="N. Virginia">
us-east-1
</SelectItem>
<SelectItem value="us-west-2" description="Oregon">
us-west-2
</SelectItem>
<SelectItem value="ca-central-1" description="Not enabled for this account" isDisabled>
ca-central-1
</SelectItem>
</SelectGroup>
</Select>
);
}In a form#
With a name, the value goes into the form's data. Inside a Form, isRequired stops an empty submit: the browser's message shows under the field and the field gets focus.
import { useState } from "react";
import { Button, Form, Select, SelectItem } from "cennge-ui";
export default function DeployForm() {
const [sent, setSent] = useState<string | null>(null);
return (
<Form
onSubmit={(event) => {
event.preventDefault();
setSent(String(new FormData(event.currentTarget).get("environment")));
}}
className="flex w-full max-w-80 flex-col gap-4"
>
<Select name="environment" label="Environment" placeholder="Pick an environment" isRequired>
<SelectItem value="production">Production</SelectItem>
<SelectItem value="preview">Preview</SelectItem>
</Select>
<Button type="submit" color="primary" className="self-start">
Deploy payload-cache
</Button>
<p aria-live="polite" className="text-sm text-muted">
{sent ? `Submitted environment=${sent}` : "Nothing submitted yet"}
</p>
</Form>
);
}Variants and sizes#
The same four variants and three sizes as Input, so a Select sits in a row of text fields without standing out.
import { Select, SelectItem, type InputVariant } from "cennge-ui";
const variants: InputVariant[] = ["flat", "bordered", "faded", "underlined"];
export default function SelectVariants() {
return (
<div className="grid w-full gap-x-6 gap-y-5 sm:grid-cols-2">
{variants.map((variant) => (
<Select key={variant} variant={variant} label={variant[0]!.toUpperCase() + variant.slice(1)} defaultValue="main">
<SelectItem value="main">main</SelectItem>
<SelectItem value="next">next</SelectItem>
</Select>
))}
<Select size="sm" label="Small" defaultValue="main">
<SelectItem value="main">main</SelectItem>
<SelectItem value="next">next</SelectItem>
</Select>
<Select size="lg" label="Large" defaultValue="main">
<SelectItem value="main">main</SelectItem>
<SelectItem value="next">next</SelectItem>
</Select>
</div>
);
}Accessibility#
The field is a combobox named by its label; the description, or the error in its place, is linked with aria-describedby. While the list is open, focus stays in it: Tab does nothing, and Escape closes the list and puts focus back on the field. The clear button is a separate button named "Clear", next in the tab order after the field.
Styling#
classNames reaches the inner parts: base, label, trigger, value, popover, clearButton, description and errorMessage. Options take className on each SelectItem.
Props#
Select#
SelectItem#
Also takes every attribute of <div>, except value, which mean something else here.
SelectGroup#
Also takes every attribute of <div>.