Deck
Browse the catalog

Build / Molecules

Select

  • molecule
  • play-fn

A dropdown built on the native <select>.

Native on purpose: it gets platform keyboard behaviour, mobile pickers, and screen reader support for free. The chevron is a real <svg> rather than a CSS background-image, so automated contrast checks can still resolve the control's background colour.

import { Select } from '@meetcard/deck'

Example

<Select label="Card type" options={[{ value: 'work', label: 'Work' }]} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Default
Source
const Default = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()} />
</div>;

A placeholder gives an empty select a meaningful initial state.

With Placeholder
Source
const WithPlaceholder = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()}
        placeholder="Choose a type…"
        defaultValue="" />
</div>;

With a description, when the options need context the labels cannot carry.

With Description
Source
const WithDescription = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()}
        description="Determines which details are shared by default." />
</div>;

In error. aria-invalid is set and the message is announced.

With Error
Source
const WithError = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()}
        error="Choose a card type to continue" />
</div>;

Disabled, with its value shown.

Disabled
Source
const Disabled = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()}
        disabled />
</div>;

One option unavailable, left in the list so it is clear it exists.

With Disabled Option
Source
const WithDisabledOption = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[...options, { value: 'team', label: 'Team (Pro only)', disabled: true }]}
        onChange={fn()} />
</div>;

Regression guard. The chevron is a real <svg>, not a CSS background-image — if it were, axe could not resolve the control's background color and would report an inconclusive contrast result.

Chevron Is Not A Background Image
Source
const ChevronIsNotABackgroundImage = () => <div style={{ maxWidth: 320 }}>
    <Select
        label="Card type"
        options={[
          { value: 'work', label: 'Work' },
          { value: 'personal', label: 'Personal' },
          { value: 'event', label: 'Event' },
        ]}
        onChange={fn()} />
</div>;

Props

PropTypeDefaultDescription
labelrequiredReactNode——
optionsrequiredSelectItem[]—Plain options, or SelectOptionGroups to render native <optgroup>s.
descriptionReactNode——
errorReactNode——
fieldClassNamestring——
hideLabelbooleanfalse—
placeholderstring—Renders a non-selectable first option, for empty-state selects.
size'sm' | 'md' | 'lg''md'—

Also accepts everything from Omit, so id, data-* and event handlers pass straight through.

Open in Storybookfor controls, the a11y panel and the interaction runner.