Select

.select shares the control rule with .input and .textarea, then replaces the browser's dropdown arrow with one of its own. The replacement is drawn with two CSS gradients rather than an image, which means it takes currentColor and follows the text into dark mode without a second asset or a second rule.

When to use it

Put it on a real <select>, wrapped in a .field with a label. Use it for one choice from a short, known list where the reader does not need to see all the options at once — a country, a currency, a status.

<div class="field" style="max-inline-size:22rem"> <label class="label" for="sel-plan">Plan</label> <select class="select" id="sel-plan"> <option>Starter</option> <option>Team</option> <option>Enterprise</option> </select> </div>
<div class="field" style="max-inline-size:22rem">
  <label class="label" for="sel-plan">Plan</label>
  <select class="select" id="sel-plan">
    <option>Starter</option>
    <option>Team</option>
    <option>Enterprise</option>
  </select>
</div>

The chevron

appearance: none;
padding-inline-end: var(--space-10);
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                  linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: calc(100% - 19px) calc(50% + 1px), calc(100% - 14px) calc(50% + 1px);
background-size: 5px 5px, 5px 5px;

Two 5×5 squares, each half-filled on a diagonal, placed side by side so the filled halves form a chevron. It is a trick, and it earns its place for three reasons:

  • No request. No SVG file, no data URI, nothing to 404 and nothing to add to the byte count beyond the declarations themselves.
  • currentColor. The arrow is the text colour, so it changes with the theme, with a disabled state, and with any colour you set on the control — automatically. A background image would need a second copy for dark mode.
  • It scales with the reader. The 5px squares are fixed, but the padding that makes room for them comes from --space-10, so the arrow stays clear of the text at any font size.

appearance: none removes the platform control's own arrow. Without it you get two. It also removes the platform's focus ring, which is why the shared rule supplies --ring — see input states.

States

All from the shared rule: hover darkens the border, focus swaps it for --focus and adds the ring, disabled sinks the background and drops the shadow. cursor: pointer is the one addition — a select is a control you click rather than one you type into.

The chevron greys out with the text, because it is currentColor
<div class="stack stack-3" style="max-inline-size:22rem"> <select class="select" aria-label="Ordinary"><option>Ordinary</option></select> <select class="select" disabled aria-label="Disabled"><option>Disabled</option></select> </div>
<div class="stack stack-3" style="max-inline-size:22rem">
  <select class="select" aria-label="Ordinary"><option>Ordinary</option></select>
  <select class="select" disabled aria-label="Disabled"><option>Disabled</option></select>
</div>

There is no readonly for a <select> — the attribute does not exist on it. If a value must be shown but not changed, use disabled and submit the value in a hidden input, or render it as text.

Multiple and size

<select multiple> and size="n" render as a list box rather than a dropdown. Deck's rule still applies its border and focus ring, but the chevron is drawn over the top-right of the list, where there is no dropdown to point at.

The chevron is still painted, which is wrong for a list box
<div class="field" style="max-inline-size:22rem"> <label class="label" for="sel-multi">Regions</label> <select class="select" id="sel-multi" multiple size="4"> <option>Europe</option> <option>North America</option> <option>Asia Pacific</option> <option>Latin America</option> </select> </div>
<div class="field" style="max-inline-size:22rem">
  <label class="label" for="sel-multi">Regions</label>
  <select class="select" id="sel-multi" multiple size="4">
    <option>Europe</option>
    <option>North America</option>
    <option>Asia Pacific</option>
    <option>Latin America</option>
  </select>
</div>

That stray chevron is a small defect, recorded in FINDINGS.md. One rule fixes it: .select[multiple], .select[size]:not([size="1"]) { background-image: none; padding-inline-end: var(--space-3); }. Multi-selects are also hard to operate — a reader has to know to hold a modifier key — so a group of .check boxes is usually the better control anyway.

Classes

.select is declared alongside .input and .textarea in one shared rule, so the extractor does not treat it as a component root and there is no completeness check on this page.

Classes, generated from the stylesheet source
Class Layer What it does Source
.select deck.components Marks a field invalid from your own validation, producing the same border and ring as :user-invalid without needing the browser to agree. One of the few states an author writes by hand. 06-forms.css:49

Tokens

Design tokens this component reads
Token Live Declared value
--control-h 42px
--surface light-dark(var(--ink-0), var(--ink-950))
--bg-sunken light-dark(var(--ink-100), oklch(8.6% .012 var(--hue-neutral)))
--line-strong light-dark(var(--ink-300), oklch(36% .018 var(--hue-neutral)))
--ink-400 oklch(70.8% calc(var(--chroma-neutral) * 2.6) var(--hue-neutral))
--focus light-dark(var(--brand-500), var(--brand-300))
--ring 0 0 0 3px light-dark(oklch(from var(--focus) l c h / .32), oklch(from var(--focus) l c h / .42))
--r-sm 7px
--shadow-1 0 1px 2px oklch(20% .03 var(--shadow-color) / .06), 0 1px 1px oklch(20% .03 var(--shadow-color) / .04)
--text-base .9375rem
--text-faint light-dark(var(--ink-500), var(--ink-500))
--space-3 .75rem
--space-10 2.5rem
--dur-1 110ms

Accessibility

  • The element is real, so everything works. appearance: none changes painting only. The control is announced as a combo box, the arrow keys move through options, typing jumps to a match, and the native option list opens in the platform's own accessible widget.
  • The chevron is decorative and unannounced. It is a background-image, so it is not in the accessibility tree and cannot be selected. That is correct — the element already tells a screen reader what it is.
  • The option list is the platform's, and that is a feature. A custom-drawn dropdown has to reimplement typeahead, focus management and touch behaviour, and usually gets one of them wrong. Deck styles the closed control and leaves the open list alone.
  • Options cannot be styled, and should not need to be. <option> styling is inconsistent across platforms and ignored on most. If the options need icons or two lines each, you need .combo, not a select.
  • A select needs a label, not just a first option. <option>Choose a plan</option> as a placeholder is not a label: it disappears on selection, and a screen reader announces it as a value.
  • 16px on touch, as with every control. Below that, iOS Safari zooms the page when the control takes focus.
  • Contrast. The chevron is currentColor, so it inherits whatever contrast the text has — which means it is compliant by construction rather than by a separate check.

Right to left

This is the one place in Deck's forms where logical properties are not enough. background-position has no logical equivalent — there is no background-position-inline-start — so the chevron cannot mirror itself. src/19-logical.css redeclares it:

[dir="rtl"] .select {
  background-position: 19px calc(50% + 1px), 14px calc(50% + 1px);
  background-image: linear-gradient(135deg, transparent 50%, currentColor 50%),
                    linear-gradient(45deg, currentColor 50%, transparent 50%);
  padding-inline: var(--space-10) var(--space-3);
}

Note that the two gradient angles are swapped as well as the positions. 45deg and 135deg exchanged is what reflects the chevron; moving it without swapping them would put a correctly-positioned arrow pointing the wrong way.

Arrow on the left, still pointing down, padding on the correct side
<div dir="rtl" class="field" style="max-inline-size:22rem"> <label class="label" for="sel-rtl">الخطة</label> <select class="select" id="sel-rtl"> <option>أساسية</option> <option>فريق</option> </select> </div>
<div dir="rtl" class="field" style="max-inline-size:22rem">
  <label class="label" for="sel-rtl">الخطة</label>
  <select class="select" id="sel-rtl">
    <option>أساسية</option>
    <option>فريق</option>
  </select>
</div>

Reduced motion

The border, shadow and background transition over 110ms; the global reset collapses them to .01ms under prefers-reduced-motion: reduce. The option list's own opening animation belongs to the platform and is outside Deck's reach — which is another argument for not reimplementing it.

Printing

src/99-print.css gives the control a plain #999 border on white, and then removes the chevron specifically:

.select { background-image: none !important; padding-inline-end: var(--space-3) !important; }

Both halves again. On paper there is no dropdown to open, so an arrow pointing at nothing is noise — and once it is gone, the padding that made room for it is a gap that makes the value look oddly indented, so that comes off too.

Overriding it

@layer app.components {
  /* A larger chevron */
  .select {
    background-size: 7px 7px, 7px 7px;
    background-position: calc(100% - 22px) calc(50% + 1px),
                         calc(100% - 15px) calc(50% + 1px);
  }

  /* No chevron at all, for a select that is styled as plain text */
  .select-bare {
    background-image: none;
    padding-inline-end: var(--space-3);
    border-color: transparent;
    box-shadow: none;
  }
}

If you move the chevron, remember the [dir="rtl"] rule exists and will now disagree with yours. That is the cost of a property with no logical form, and it is the reason this page spends a section on it.

When not to use it

  • Not for a long list. A select with two hundred options is unusable — no filtering, no search, a scroll list the reader has to read. Use .combo, which filters as they type.
  • Not for two or three options. A select hides the choices behind a click. .segmented or a group of .check radios shows them all immediately, and a reader can compare them without opening anything.
  • Not for multiple selection. <select multiple> requires a modifier key most people do not know about, and on touch it is worse. Use checkboxes, or .combo with multiple selection.
  • Not for a boolean. Yes/No in a dropdown is two clicks for something .switch or a single checkbox does in one.
  • Not when the options need formatting. Icons, two-line entries, grouped headings with descriptions — <option> cannot carry any of it reliably. That is what .combo is for.