Skip to content
cennge-ui
GitHubv0.3.1, changelog

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
  1. Open it with a click, Enter, Space or an arrow key; the chosen option carries the check.
  2. On the closed field, type a letter: the value jumps to the next option that starts with it.
  3. The × sets Language to null: the placeholder returns and the list shows every repository.
Live: the component itself, not a picture of it.

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 value and onValueChange, null for none; or uncontrolled with defaultValue
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

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.

value is null
basic.tsx
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.

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

Nothing submitted yet

in-a-form.tsx
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.

variants.tsx
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#

PropTypeDefault
label
React.ReactNode
–
placeholder
React.ReactNode
–
description
React.ReactNode
–
errorMessage
React.ReactNode
–
value
string | nullSelected value. null shows the placeholder.
–
defaultValue
string | null
–
onValueChange
(value: string | null) => void
–
isClearable
booleanShows a clear button that returns to the placeholder.
–
isInvalid
boolean
–
isRequired
boolean
–
isDisabled
boolean
–
isOpen
boolean
–
onOpenChange
(isOpen: boolean) => void
–
variant
"bordered""flat""faded""underlined"
"flat"
color
"default""primary""secondary""success""warning""danger""info"Focus colour.
"primary"
size
"sm""md""lg"
"md"
radius
"none""sm""md""lg""full"
"md"
startContent
React.ReactNode
–
name
stringForm field name.
–
id
string
–
className
string
–
classNames
Partial<Record<SelectSlots, string>>
–
aria-label
string
–
children
React.ReactNodeSelectItem, SelectGroup, SelectSeparator.
–

SelectItem#

PropTypeDefault
valuerequired
stringMust be non-empty.
–
textValue
stringText used for the trigger and typeahead when children are not plain text.
–
description
React.ReactNode
–
startContent
React.ReactNode
–
isDisabled
boolean
–

Also takes every attribute of <div>, except value, which mean something else here.

SelectGroup#

PropTypeDefault
label
React.ReactNode
–

Also takes every attribute of <div>.