# Select

A choice from a list: the input well with a caret, opening the dropdown panel with one item per choice, in groups under a heading if wanted, each choice with a picture or an icon and a second line if wanted.

Block: `smbk-select`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `md`, `sm` | `md` | The input’s two heights, with the dropdown’s items at the same size when open. Small text never goes under 16 px on a phone. |
| `error` | on / off | off | The choice is invalid. Draws the danger border, kept while focused, and the error icon before the caret. |
| `open` | on / off | off | The list is showing. Set by the component while it is open; given to the wrapper, it holds the list open, as in a picture of the page. |

Parts: `smbk-select__well`, `smbk-select__value`, `smbk-select__icon`, `smbk-select__list`, `smbk-select__group`, `smbk-select__option`, `smbk-select__option-lead`, `smbk-select__option-text`, `smbk-select__option-label`, `smbk-select__option-description`, `smbk-select__check`

## Markup

```html
<span class="smbk-select smbk-select--size-md">
  <button
    type="button"
    id="role"
    class="smbk-input smbk-input--size-md smbk-select__well"
    role="combobox"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-label="Role"
  >
    <span class="smbk-select__value smbk-select__value--placeholder">Choose a role</span>
    <i
      class="ph ph-caret-down smbk-icon smbk-icon--size-md smbk-input__icon smbk-select__icon"
      aria-hidden="true"
    ></i>
  </button>
</span>
```

## React

```tsx
import { Select } from './design-system/components/select/select.js';

<Select size="md" id="role" aria-label="Role" placeholder="Choose a role" options={[{ value: "viewer", label: "Viewer" }, { value: "editor", label: "Editor" }, { value: "owner", label: "Owner", disabled: true }]} />
```

## Stylesheet

```css
/* smbk-select — Select. Figma "Select" 4253:2857 (State × Size: md, sm).
 * Closed, it is the input: Figma builds it on an Input instance, and here the
 * well is a button that carries smbk-input, so its chrome, hover, focus,
 * error, read-only, disabled and small size, and the sizes of its caret and
 * error icon, are the input's own and follow any change to it. Open, it is the dropdown: Figma builds the list on a Dropdown
 * instance, and here the list carries smbk-dropdown and each choice is a
 * dropdown item, so the panel, the rows, their two heights and their icons
 * are the dropdown's own. This stylesheet only places the list under the
 * well, at the well's width, no taller than the list height, and lets it scroll, and draws what the dropdown has no
 * part for: options in groups under the dropdown's label, and the rich option (Figma's "RichOption" 8204:3194). The
 * label and the help text around a select are the field's. */

.smbk-select {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  width: 100%;
}

/* Only what a button needs on top of the input's well. */
.smbk-select__well {
  margin: 0;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}

.smbk-input.smbk-select__well:not(:disabled, [aria-readonly="true"]) {
  cursor: pointer;
}

/* A read-only select does not open, so it shows no pointer. */
.smbk-input.smbk-select__well[aria-readonly="true"] {
  cursor: default;
}

/* The value is cut short with an ellipsis, so the clip is across only: nothing above or below the line is cut. */
.smbk-select__value {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: clip;
  overflow-y: visible;
  overflow-clip-margin: var(--smbk-space-inset-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.smbk-select__value--placeholder {
  color: var(--smbk-color-text-subtlest);
}

.smbk-select__well:disabled .smbk-select__value--placeholder {
  color: var(--smbk-color-text-disabled);
}

/* The caret is the input's icon part, so it takes the field's icon size: 20 at md, 16 at sm. */
.smbk-select__icon {
  flex-shrink: 0;
}

/* ---- the open list: the dropdown panel under the well, as wide as the well ----
   Paired with the dropdown's block, so these win over its width whatever order the stylesheets load in: the list
   keeps the well's width, narrower or wider than a dropdown, and a long choice wraps inside it. Lifted over the
   page, the dropdown's layer takes it and the wrapper sets its place and width. */

.smbk-dropdown.smbk-select__list {
  position: absolute;
  top: calc(var(--smbk-size-control-md) + var(--smbk-space-stack-xs));
  left: 0;
  z-index: var(--smbk-layer-overlay);
  width: 100%;
  min-width: 0;
  max-width: none;
  list-style: none;
  outline: none;
}

/* A list longer than the list height, or than its room, stops there and scrolls inside itself: the dropdown
   panel's own rule, with its shade at the foot. The wrapper caps it further to the room it has. */

/* The keyboard's highlight looks as the pointer's hover does: the dropdown item's hover fill. While the
   pointer's fill shows (the dropdown's glide), it is the highlight, so the choice draws no second one. */
.smbk-select__option--highlighted {
  background: var(--smbk-color-surface-hover);
}

.smbk-select__list[data-glide="lit"] .smbk-select__option--highlighted {
  background: transparent;
}

/* While the well has focus and the keys moved the highlight, the highlighted choice is where focus is: it
   carries the focus ring, drawn just inside its edge, because the fill alone is too faint to find. A choice
   under the pointer takes the hover fill alone, as any item under the pointer does. */
.smbk-select:focus-within [data-highlight="keys"] .smbk-select__option--highlighted {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: calc(-1 * var(--smbk-focus-ring-width));
}

/* A choice stays on one line, cut short with an ellipsis, so every row keeps its height. The clip is
   across only, so the descenders stay whole in every browser (WebKit does not honour the clip margin). */
.smbk-select__option-label {
  overflow-x: clip;
  overflow-y: visible;
  overflow-clip-margin: var(--smbk-space-inset-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- groups: the dropdown's label heads its options, and the dropdown's divider comes before each group that
   follows something. The group's element only holds them; the options' own list is the dropdown's group, spaced as
   the panel is, so grouping adds nothing on screen but the heading and the line. ---- */

.smbk-select__group {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.smbk-select__group > .smbk-dropdown__group {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ---- the rich option: an avatar or an icon, the label with a second line under it, and the tick when chosen
   (Figma's RichOption). One height at either size, as Figma draws one: the small inset above and below, the large
   inset at the sides and the medium gap, the label in Body MD and the line under it in Body SM, subtle, with no
   space between them; the icons at the medium size. Written against the list and the dropdown's item, so it wins
   over the small item whatever order the stylesheets load in. ---- */

.smbk-select__list .smbk-dropdown__item.smbk-select__option--rich {
  gap: var(--smbk-space-inline-md);
  padding: var(--smbk-space-inset-sm) var(--smbk-space-inset-lg);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
}

.smbk-select__list .smbk-select__option--rich .smbk-dropdown__icon {
  font-size: var(--smbk-size-icon-md);
}

.smbk-select__option-lead {
  flex-shrink: 0;
}

.smbk-select__option-text {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-text-label);
}

/* The second line stays on one line, cut short as the label is, so every rich row keeps its height. */
.smbk-select__option-description {
  overflow-x: clip;
  overflow-y: visible;
  overflow-clip-margin: var(--smbk-space-inset-xs);
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A disabled option dims its second line with its label, and its avatar as Figma binds it. The combobox's option sets
   the same line after its label. */
.smbk-select__option[aria-disabled="true"] .smbk-select__option-description,
.smbk-combobox__option[aria-disabled="true"] .smbk-select__option-description {
  color: var(--smbk-color-text-disabled);
}

.smbk-select__option[aria-disabled="true"] .smbk-avatar.smbk-select__option-lead {
  opacity: var(--smbk-opacity-disabled);
}

/* ---- small: the well is the input's small size and the rows the dropdown's small items; the list follows the well's height ---- */

.smbk-select--size-sm .smbk-select__list {
  top: calc(var(--smbk-size-control-sm) + var(--smbk-space-stack-xs));
}

/* ---- above: with more room over the well than under it, the wrapper opens the list upward, the same gap
   away (data-side="above"), and places it. ---- */

/* ---- motion: the list is the dropdown panel, so it drops from the well across the gap and fades in with
   the dropdown's enter motion, or rises to it when it opens above. It closes at once, because it leaves the
   page as it closes. No movement under reduced motion (the dropdown's own rule). ---- */

/* ---- touch: the well takes an invisible press area ----
   On a touch screen an invisible area, centred on it, takes the press out to the comfortable target in each direction
   it falls short of it; its look, its focus ring and its place stay as drawn. */

@media (pointer: coarse) {
  .smbk-select__well {
    position: relative;
  }

  .smbk-select__well::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  /* The keyboard's highlight in the open list. */
  .smbk-dropdown__item.smbk-select__option--highlighted {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }

  .smbk-select__option--highlighted .smbk-dropdown__icon,
  .smbk-select__option--highlighted .smbk-select__option-description {
    color: inherit;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inset": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Padding inside a component, small."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      }
    },
    "stack": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things stacked one above another, extra small."
      }
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    }
  },
  "color": {
    "text": {
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "surface": {
      "hover": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.15}}",
        "$description": "A row or option under the pointer or the keyboard highlight: menu options, table rows. Translucent, so it works on any surface."
      }
    },
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    }
  },
  "size": {
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "layer": {
    "overlay": {
      "$type": "number",
      "$value": 1200
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      }
    }
  },
  "typography": {
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "opacity": {
    "disabled": {
      "$type": "number",
      "$value": 0.5,
      "$description": "Fade for disabled pictures and media (50%)."
    }
  }
}
```

## Purpose

A choice from a fixed list, shown closed as the input well with a caret and open as the dropdown panel under it, one dropdown item per choice. It looks the same in every browser, which the native list does not, and it still works with a keyboard and a screen reader.

## Anatomy

- The block, holding the well and, while open, the list.
- The well part, a button that is the input's well: it carries the input's classes, so its chrome and states are the input's.
- The value part, the chosen choice's label or the placeholder.
- The icon part, the caret, at the well's end, at the field's icon size (it is the input's icon part too). A read-only select has none.
- In error, the input's error icon, a warning sign before the caret.
- The list part, the dropdown panel itself (it carries the dropdown's classes at the select's size), of option parts, each a dropdown item with its option label part and, on the chosen one, the check part.
- Optionally, group parts: each holds the dropdown's label as the group's heading and the dropdown's group of the options under it, with the dropdown's divider before each group that follows something.
- Optionally, on an option, the option lead part before the label, a small avatar or an icon, and the option description part, a second line under the label; the label and the second line sit in the option text part. An option with a second line or an avatar is the rich option, a taller row.
- The label and the help text are not the select's: put it in a field.

## When to use

- Choosing one of a handful of known values: a country, a role, a sort order.
- Choices that fall into a few named groups, such as offices by region: give the groups headings.
- Choosing a person or a thing that needs more than a name to tell apart: give each choice a picture or an icon and a second line, such as a person's role.

## When not to use

- Two or three choices that should all be visible. Use [radios](radio.md) or the [segmented control](segmented-control.md).
- A long list to search through. Use a [combobox](combobox.md).
- Multiple choices. Use [checkboxes](checkbox.md).

## Behavior

The closed well behaves as the input does: it lifts on hover, shows the field focus border when focused, and on error draws the danger border, kept while focused, and the warning icon. Any change to the input's look reaches the select. It comes in the input's two sizes, md and sm. A read-only select shows its choice flat on the page with a faint edge and no caret; it stays in the tab order but never opens, and its value still travels with the form. Pressing the well opens the dropdown panel under it with one item per choice, as tall as the dropdown's items at the same size (the small select has the small items); the chosen one carries a check. Arrow keys move the highlight, Home and End jump to the first and last choice that can be chosen (on a closed select they open the list there), Enter and Space choose; a disabled choice is passed over. Escape, a click outside, or focus leaving the select closes the list without choosing; Tab closes it and moves on to the next control, as it would from a closed select. Typing jumps to the next choice that starts with the letters typed (they add up while typed quickly; the same letter again steps through the choices that start with it), and opens a closed list on it.
With a mouse, the highlight is the dropdown's sliding hover fill: it follows the pointer from choice to choice, lights the nearest choice when the pointer is in the list's padding, where a click chooses it, and steps aside while the keys move, so the keyboard's highlight keeps its own fill and focus ring. The list drops in from the well, growing from its edge nearest the well (upward when it opens above), and fades in with the enter motion, as the dropdown panel does, and closes at once; under reduced motion it fades in without moving.

The open list shows over the whole page, so a part around the select that clips or scrolls, such as an open accordion panel, never cuts it off, and it keeps the theme, density and text size of the place it opened from. It is as wide as the well, never taller than the system's list height, and never makes the page longer or runs off the screen: it stops the list's own gap short of the window's edge and of a boundary the page may name (such as the part of the page whose end the list should not pass), and stays beside the well as the page scrolls. A longer list scrolls inside itself with the system's scrollbar, a thin, see-through bar over its choices that takes no room and shows while the mouse is over it or it scrolls, with a soft shade along its foot while more choices wait below, and scrolling it never hands on to the page. Where there is more room above the well than under it, the list opens upward and rises into place. The placing uses React Aria, which a React project installs beside React. It opens scrolled to the chosen choice, and the keyboard's highlight is always kept in view. A short list that fits looks exactly as it did. The value can be controlled from outside or left to the component, and travels in a hidden native select when the select has a form name, hidden from view and from screen readers. Give `autoComplete` the kind of value it holds, such as `country-name`, and the hidden select carries it, so the browser's autofill can fill it. A disabled select sends nothing with its form, as a disabled native control does. A required select stops its form being sent while no choice is made: the well takes focus and its error look, which stays until a choice is made; the page words the message in the field. The form's reset puts the starting choice back.

Choices can come in groups: each group's heading sits over its choices in the dropdown's small grey label, and a line divides it from what comes before. The headings are not choices: the arrows, Home, End and typing pass from the last choice of one group to the first of the next. A choice can also carry a picture or an icon before its label and a second line under it; with a second line or a picture, it is the taller rich row, the same at both sizes, its second line in the smaller, quieter text and cut short on one line as the label is. A choice given a picture address shows the picture in a small avatar, and its initials when the address is empty or the picture fails to load. The well shows only the chosen choice's label. A disabled rich choice dims its second line and its picture with its label.

The page can hold the list open or shut by giving the React wrapper `open`, as for a test or a picture of the page; `onOpenChange` then hears each request to open or close it (a press, a key, a choice, a click outside, focus leaving), and the page decides. Left out, the select opens and closes itself. A disabled or read-only select never opens, whatever `open` says.

A ref given to the React wrapper reaches the well, the button that opens the list, so a page can focus it. The select keeps working as usual with a ref attached.

A page names the boundary the list should not pass by giving `boundary` a ref to any element.

## Accessibility

### What it does for you

- The well is a combo box button that opens a listbox. In development, a select with no name logs a warning in the console.
- A read-only select says so with `aria-readonly`.
- The list is in the page only while it is open, and the well names it as the element it controls only then, as ARIA 1.2 asks of a combo box, so the well never points at something that is not there.
- The highlighted option is announced through the active descendant while the list is open, so focus never leaves the well. While the well has focus, the highlighted option carries the focus ring just inside its edge, so it can be found without telling the fills apart.
- Typing to jump works as the select-only combo box pattern describes; a space while a word is being typed is part of the word, not a choice.
- An error marks the well invalid as well as drawing the danger border.
- In a forced-color mode, the keyboard's highlight in the open list is drawn in the system's highlight colors, its second line included.
- A group of choices is a group in the listbox named by its heading, so a screen reader says which group a choice is in. The picture before a rich choice is not read out: the label names it. In the hidden native select the groups are option groups with the same headings.

### What you must do

- Give the well a label, with `aria-label` or `aria-labelledby`, or by a field.
- Put it in a field, which links its help text or error message to the well through `aria-describedby`, and whose required mark comes with `aria-required` on the well.

### How to test

- Tab to the select, then press Enter, Space or Down Arrow: the list opens on the chosen choice. Move with the arrow keys, Home and End, and press Enter to choose. Press Escape to close it without choosing.
- Type the first letters of a choice: the highlight jumps to it. Typing in a closed select opens the list on it.
- With a screen reader, the well is read as a combo box with its label and its value, and the highlighted option is read as the arrows move, with its group's heading when the arrows enter a group.
- A disabled choice is passed over by the arrows. A read-only select is reachable with Tab but never opens.
- With text at 200% and the page 320 pixels wide, the well and the open list stay inside the window, and a long list scrolls inside itself.
- In a forced-color mode, the highlighted choice in the open list is still told from the others.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the select. With the list open, closes it, chooses nothing and moves on. |
| Enter or Space | Opens the list when it is closed. With it open, chooses the highlighted choice. A space while a word is being typed is part of the word. |
| Down Arrow or Up Arrow | Opens the list when it is closed. With it open, moves the highlight down or up, passing over a disabled choice. |
| Home or End | Moves the highlight to the first or last choice that can be chosen, opening the list when it is closed. |
| Any character | Jumps to the next choice that starts with the letters typed, and opens a closed list on it. |
| Escape | Closes the list without choosing. |

## Usage rules

### Do

- Put the most likely value first, or a placeholder that says what to choose.

### Don't

- Use a select for navigation. That is a [dropdown](dropdown.md).
