Skip to content

role combobox

Select draft v0.1.0

Lets people choose one option from a list that opens on demand.

A select shows the current choice in a closed box and opens a list of options when activated; the browser or the platform renders the list, its keyboard interaction and its announcements. It fits a choice among many known values (a country, a time zone, a sort order) where showing every option at once would take too much room. Two to about six options worth reading side by side are a Radio group; a list of actions is a Dropdown.

Specification

From packages/components/specs/components/select.spec.json

Raw file packages/components/specs/components/select.spec.json
{
  "$schema": "../schema/component.schema.json",
  "id": "select",
  "name": "Select",
  "version": "0.1.0",
  "status": "draft",
  "role": "combobox",
  "summary": "Lets people choose one option from a list that opens on demand.",
  "description": "A select shows the current choice in a closed box and opens a list of options when activated; the browser or the platform renders the list, its keyboard interaction and its announcements. It fits a choice among many known values (a country, a time zone, a sort order) where showing every option at once would take too much room. Two to about six options worth reading side by side are a Radio group; a list of actions is a Dropdown.",
  "tags": [
    "form",
    "input",
    "selection"
  ],
  "anatomy": [
    {
      "id": "container",
      "role": "container",
      "required": true,
      "description": "Root. Stacks the label, the control, the description and the error message; carries the size and the forced state."
    },
    {
      "id": "label",
      "role": "label",
      "required": true,
      "description": "Text naming the field. Provides the accessible name; activating it focuses the control."
    },
    {
      "id": "field",
      "role": "container",
      "required": true,
      "description": "Box holding the control and the chevron drawn over its end edge."
    },
    {
      "id": "control",
      "role": "value",
      "required": true,
      "parent": "field",
      "description": "The native select, drawn by the component as a closed box showing the chosen option. Receives focus, holds the value; the browser opens and renders the list."
    },
    {
      "id": "indicator",
      "role": "indicator",
      "required": true,
      "decorative": true,
      "parent": "field",
      "description": "Chevron glyph of the icon font at the end edge of the control, saying that it opens a list. Hidden from assistive technology, which learns it from the role."
    },
    {
      "id": "option",
      "role": "value",
      "required": true,
      "repeatable": true,
      "parent": "control",
      "description": "One choice in the list, rendered by the browser: the component gives it no style of its own.",
      "properties": [
        {
          "id": "label",
          "name": "Label",
          "type": "string",
          "required": true,
          "description": "Text of the option, what people read and what is announced."
        },
        {
          "id": "value",
          "name": "Value",
          "type": "string",
          "description": "Value submitted when the option is chosen. Defaults to the option's position."
        },
        {
          "id": "selected",
          "name": "Selected",
          "type": "boolean",
          "default": false,
          "description": "Whether this option is the current choice."
        },
        {
          "id": "disabled",
          "name": "Disabled",
          "type": "boolean",
          "default": false,
          "description": "The option stays in the list, dimmed, and cannot be chosen."
        }
      ]
    },
    {
      "id": "description",
      "role": "description",
      "required": false,
      "description": "Optional help text under the control (format, example, consequence), associated with it as its description."
    },
    {
      "id": "error-message",
      "role": "errormessage",
      "required": false,
      "visibleIn": [
        "invalid"
      ],
      "description": "Text describing the error when the field is invalid, associated with the control as a description. Shown only while invalid."
    }
  ],
  "properties": [
    {
      "id": "size",
      "name": "Size",
      "type": "enum",
      "default": "medium",
      "description": "Control height, inline padding and text size scale together; label, description and error keep their size.",
      "affects": [
        "control",
        "indicator"
      ],
      "options": [
        {
          "value": "small",
          "description": "Dense forms, toolbars, table rows (32 px high)."
        },
        {
          "value": "medium",
          "description": "Default (40 px)."
        },
        {
          "value": "large",
          "description": "Prominent, single fields: a search page, a sign-in form (48 px)."
        }
      ]
    },
    {
      "id": "label",
      "name": "Label",
      "type": "string",
      "required": true,
      "bindsTo": "label",
      "description": "What the field asks for, as a noun phrase (Email address). Always visible."
    },
    {
      "id": "options",
      "name": "Options",
      "type": "list",
      "required": true,
      "of": [
        "option"
      ],
      "bindsTo": "option",
      "description": "The choices, in order."
    },
    {
      "id": "placeholder",
      "name": "Placeholder",
      "type": "string",
      "description": "Text shown while no option is chosen (Choose a country). A disabled, unselectable first option: never the label."
    },
    {
      "id": "description",
      "name": "Description",
      "type": "string",
      "bindsTo": "description",
      "description": "Help text: expected format, example, or what the value is used for."
    },
    {
      "id": "errorMessage",
      "name": "Error message",
      "type": "string",
      "bindsTo": "error-message",
      "description": "What is wrong and how to fix it, shown when the field is invalid."
    },
    {
      "id": "required",
      "name": "Required",
      "type": "boolean",
      "default": false,
      "description": "The field must be filled to submit the form."
    },
    {
      "id": "invalid",
      "name": "Invalid",
      "type": "boolean",
      "default": false,
      "description": "The value failed validation: the border takes the error color and the error message is shown."
    },
    {
      "id": "disabled",
      "name": "Disabled",
      "type": "boolean",
      "default": false,
      "description": "Non-interactive and skipped by the keyboard; the value stays visible. Prefer read-only when people may need to read or copy the value."
    }
  ],
  "states": [
    {
      "id": "default",
      "trigger": "interaction",
      "description": "Resting: an outlined box on the default surface."
    },
    {
      "id": "hover",
      "trigger": "interaction",
      "inputs": [
        "pointer"
      ],
      "description": "Pointer over the control. The box takes the surface hover tint; the border does not change. Needs a pointing device: touch has no hover."
    },
    {
      "id": "focus-visible",
      "trigger": "interaction",
      "description": "Keyboard focus on the control. Shows the shared focus ring outside its border; pointer focus shows none.",
      "inputs": [
        "keyboard"
      ]
    },
    {
      "id": "invalid",
      "trigger": "property",
      "property": "invalid",
      "description": "Validation failed: the border takes the error color and the error message is shown under the control."
    },
    {
      "id": "disabled",
      "trigger": "property",
      "property": "disabled",
      "description": "Non-interactive. Box, value, label and description drop to the shared disabled colors; hover and focus do not apply."
    }
  ],
  "layout": {
    "direction": "vertical",
    "align": "stretch",
    "justify": "start",
    "wrap": false,
    "sizing": "fill",
    "notes": [
      "Label, control, description and error message stack, separated by container.gap; the control fills the width of the component, which the layout sizes.",
      "Inline padding, minimum height and text size come from the size option; the corner radius and the border width do not change.",
      "The chevron sits over the end edge of the control, which reserves room for it with padding-inline-end."
    ]
  },
  "accessibility": {
    "ariaRole": "combobox",
    "name": {
      "text": "Computed from the label, associated with the control as its label element on the web. The description and the error message are exposed as the accessible description, not as part of the name.",
      "refs": [
        "wai-labels",
        "html-aam-name-other",
        "apg-describing"
      ]
    },
    "keyboard": [
      {
        "key": "Tab",
        "action": "Moves focus to the control. A disabled control is not focusable and is skipped.",
        "refs": [
          "wcag:2.1.1",
          "html-focusable-area",
          "html-disabled"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "key": "Space, Enter, arrow keys",
        "action": "Open the list and move through the options the way the platform's native select does; the browser owns this interaction.",
        "refs": [
          "html-select",
          "mdn-select"
        ],
        "platforms": [
          "web"
        ]
      }
    ],
    "guidelines": [
      {
        "text": "Expose the combobox role, the name, the value and the required, disabled and invalid states through the platform's native select, so assistive technology receives them without extra work.",
        "refs": [
          "wcag:4.1.2",
          "aria-combobox"
        ]
      },
      {
        "text": "Use the native select element rendered as a drop-down box: it exposes the combobox role, its value and states, and the browser owns the list, its keyboard interaction and its announcements. The list is not styled by the component.",
        "refs": [
          "html-aria-select",
          "html-aam-select",
          "html-select",
          "mdn-select"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "Every field has a visible label associated with the control programmatically; help text that people need to fill the field (format, example) is visible too, as the description.",
        "refs": [
          "wcag:3.3.2",
          "wcag:1.3.1",
          "wai-labels",
          "wai-instructions",
          "html-label"
        ]
      },
      {
        "text": "Associate the label with a label element whose for attribute names the control's id; reference the description and the error message with aria-describedby, so they are read after the name.",
        "refs": [
          "wcag-h44",
          "wcag-aria1",
          "apg-describing"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "Use a pop-up button to present a flat list of mutually exclusive options or states.",
        "refs": [
          "hig-pop-up-buttons"
        ],
        "platforms": [
          "ios"
        ]
      },
      {
        "text": "The chevron is decorative and hidden from assistive technology with aria-hidden=\"true\": an icon-font glyph is written as its ligature name, which would otherwise be read with the value.",
        "refs": [
          "wcag:1.1.1",
          "aria-hidden"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "When the value is invalid, identify the field and describe the error in text next to it: the error message names what is wrong and how to fix it, and is associated with the control.",
        "refs": [
          "wcag:3.3.1",
          "wai-notifications",
          "wcag-g83"
        ]
      },
      {
        "text": "Say that the value is invalid with aria-invalid on the control, and that a field is required with the required attribute, which the browser exposes as required.",
        "refs": [
          "wcag-aria21",
          "aria-invalid",
          "html-required",
          "html-aam-required",
          "wai-validation"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "Mark required fields in the label or the description, never by the error color alone.",
        "refs": [
          "wcag-h90",
          "wcag-f81",
          "wcag:1.4.1"
        ]
      },
      {
        "text": "Label, value, description and error text contrast at least 4.5:1 with their background in every color mode and enabled state: label and value 19.07:1 (light) and 11.27:1 (dark) on the default surface, description 12.87:1 and 8.80:1, placeholder 6.08:1 and 6.36:1, error text 4.52:1 and 4.97:1 (4.26:1 on a level 1 surface in light mode, a Figma library limit tracked as a known issue). Disabled text is inactive and exempt.",
        "refs": [
          "wcag:1.4.3"
        ]
      },
      {
        "text": "The border is what identifies the field, so it contrasts at least 3:1 with the adjacent surface in every color mode: 12.87:1 (light) and 8.80:1 (dark) on the default surface at rest, 12.14:1 and 8.06:1 on the hover tint; the error border 6.12:1 and 3.82:1; the focus ring 5.79:1 and 12.80:1. Disabled is exempt.",
        "refs": [
          "wcag:1.4.11",
          "understanding-non-text-contrast-boundaries"
        ]
      },
      {
        "text": "A visible focus indicator appears when the control receives focus, drawn outside its border.",
        "refs": [
          "wcag:2.4.7",
          "wcag:2.4.13"
        ]
      },
      {
        "text": "The focus ring is drawn with :focus-visible, so pointer focus on the closed control shows no ring.",
        "refs": [
          "mdn-focus-visible"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "Every size is a pointer target of at least 24 x 24 CSS pixels through its minimum height: 32, 40 and 48 pixels.",
        "refs": [
          "wcag:2.5.8"
        ]
      },
      {
        "text": "A disabled select leaves the focus order; a disabled option stays in the list, announced as dimmed, and cannot be chosen.",
        "refs": [
          "html-disabled",
          "apg-disabled-focus"
        ],
        "platforms": [
          "web"
        ]
      },
      {
        "text": "Apple recommends a hit region of at least 44 x 44 pt; the small and medium sizes (32 and 40 pt) fall under it on iOS today: the Figma library has no platform heights.",
        "refs": [
          "hig-accessibility-mobility"
        ],
        "platforms": [
          "ios"
        ]
      },
      {
        "text": "Android recommends a touch target of at least 48 x 48 dp; the small and medium sizes (32 and 40 dp) fall under it on Android today. Expose an invalid value with the error semantics so the message is announced with the field.",
        "refs": [
          "android-large-controls",
          "compose-target-sizes",
          "compose-error-components"
        ],
        "platforms": [
          "android"
        ]
      },
      {
        "text": "Color transitions of the control are removed when the user asks for reduced motion.",
        "refs": [
          "wcag:2.3.3",
          "mdn-reduced-motion"
        ]
      }
    ],
    "references": [
      {
        "id": "aria-combobox",
        "title": "WAI-ARIA 1.2: combobox role",
        "url": "https://www.w3.org/TR/wai-aria-1.2/#combobox"
      },
      {
        "id": "aria-invalid",
        "title": "WAI-ARIA 1.2: aria-invalid",
        "url": "https://www.w3.org/TR/wai-aria-1.2/#aria-invalid"
      },
      {
        "id": "apg-describing",
        "title": "APG: Describing with aria-describedby",
        "url": "https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/#describing_with_aria-describedby"
      },
      {
        "id": "apg-disabled-focus",
        "title": "APG: Focusability of disabled controls",
        "url": "https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls"
      },
      {
        "id": "html-aria-select",
        "title": "ARIA in HTML: select element (role combobox as a drop-down box)",
        "url": "https://www.w3.org/TR/html-aria/#el-select"
      },
      {
        "id": "html-aam-select",
        "title": "HTML-AAM: select element rendered as a drop-down box (combobox)",
        "url": "https://www.w3.org/TR/html-aam-1.0/#el-select-combobox"
      },
      {
        "id": "html-aam-name-other",
        "title": "HTML-AAM: accessible name computation of other form elements (select: the label element)",
        "url": "https://www.w3.org/TR/html-aam-1.0/#other-form-elements-accessible-name-computation"
      },
      {
        "id": "html-aam-required",
        "title": "HTML-AAM: required attribute (aria-required)",
        "url": "https://www.w3.org/TR/html-aam-1.0/#att-required"
      },
      {
        "id": "html-label",
        "title": "HTML: the label element (for attribute, labeled control)",
        "url": "https://html.spec.whatwg.org/multipage/forms.html#the-label-element"
      },
      {
        "id": "html-required",
        "title": "HTML: the required attribute",
        "url": "https://html.spec.whatwg.org/multipage/input.html#attr-input-required"
      },
      {
        "id": "html-select",
        "title": "HTML: the select element",
        "url": "https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element"
      },
      {
        "id": "html-disabled",
        "title": "HTML: disabled form controls",
        "url": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled"
      },
      {
        "id": "html-focusable-area",
        "title": "HTML: focusable area",
        "url": "https://html.spec.whatwg.org/multipage/interaction.html#focusable-area"
      },
      {
        "id": "mdn-select",
        "title": "MDN: the select element (labeling, accessibility)",
        "url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select#accessibility"
      },
      {
        "id": "mdn-focus-visible",
        "title": "MDN: :focus-visible",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible"
      },
      {
        "id": "mdn-reduced-motion",
        "title": "MDN: prefers-reduced-motion",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion"
      },
      {
        "id": "wai-labels",
        "title": "WAI tutorials: Labeling controls",
        "url": "https://www.w3.org/WAI/tutorials/forms/labels/"
      },
      {
        "id": "wai-instructions",
        "title": "WAI tutorials: Form instructions (required fields, placeholder text, aria-describedby)",
        "url": "https://www.w3.org/WAI/tutorials/forms/instructions/"
      },
      {
        "id": "wai-validation",
        "title": "WAI tutorials: Validating input (the required attribute)",
        "url": "https://www.w3.org/WAI/tutorials/forms/validation/"
      },
      {
        "id": "wai-notifications",
        "title": "WAI tutorials: User notifications (error messages)",
        "url": "https://www.w3.org/WAI/tutorials/forms/notifications/"
      },
      {
        "id": "wcag-h44",
        "title": "WCAG technique H44: label elements to associate text labels with form controls",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/html/H44"
      },
      {
        "id": "wcag-h90",
        "title": "WCAG technique H90: indicating required form controls",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/html/H90"
      },
      {
        "id": "wcag-g83",
        "title": "WCAG technique G83: text descriptions for required fields not completed",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/general/G83"
      },
      {
        "id": "wcag-aria1",
        "title": "WCAG technique ARIA1: aria-describedby for a descriptive label",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA1"
      },
      {
        "id": "wcag-aria21",
        "title": "WCAG technique ARIA21: aria-invalid to indicate an error field",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA21"
      },
      {
        "id": "wcag-f81",
        "title": "WCAG failure F81: required or error fields identified by color only",
        "url": "https://www.w3.org/WAI/WCAG22/Techniques/failures/F81"
      },
      {
        "id": "understanding-non-text-contrast-boundaries",
        "title": "Understanding WCAG 1.4.11 Non-text Contrast: user interface components and boundaries",
        "url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html#boundaries"
      },
      {
        "id": "hig-pop-up-buttons",
        "title": "Apple HIG: Pop-up buttons, best practices (a flat list of mutually exclusive options)",
        "url": "https://developer.apple.com/design/human-interface-guidelines/pop-up-buttons#Best-practices"
      },
      {
        "id": "hig-accessibility-mobility",
        "title": "Apple HIG: Accessibility, mobility (control sizes)",
        "url": "https://developer.apple.com/design/human-interface-guidelines/accessibility#Mobility"
      },
      {
        "id": "android-large-controls",
        "title": "Android accessibility: use large, simple controls",
        "url": "https://developer.android.com/guide/topics/ui/accessibility/apps#large-controls"
      },
      {
        "id": "compose-target-sizes",
        "title": "Jetpack Compose accessibility: minimum touch target sizes",
        "url": "https://developer.android.com/develop/ui/compose/accessibility/api-defaults#minimum-target-sizes"
      },
      {
        "id": "compose-error-components",
        "title": "Jetpack Compose accessibility: error semantics of components",
        "url": "https://developer.android.com/develop/ui/compose/accessibility/semantics#error-components"
      },
      {
        "id": "aria-hidden",
        "title": "WAI-ARIA 1.2: aria-hidden",
        "url": "https://www.w3.org/TR/wai-aria-1.2/#aria-hidden"
      }
    ]
  },
  "content": {
    "guidelines": [
      "The label is a noun phrase naming the choice: Country, Time zone, Sort by. Sentence case, no colon, no trailing punctuation.",
      "Option labels are short and parallel; order them logically (alphabetical for long lists, most common first for short ones).",
      "The placeholder, when any, is an invitation to choose (Choose a country), shown until a choice is made; it is never a valid value.",
      "The error message says what to do: Choose a country."
    ]
  },
  "examples": [
    {
      "id": "medium",
      "name": "Medium",
      "props": {
        "size": "medium",
        "label": "Country",
        "placeholder": "Choose a country",
        "options": [
          {
            "label": "France",
            "value": "fr"
          },
          {
            "label": "Germany",
            "value": "de"
          },
          {
            "label": "Spain",
            "value": "es"
          },
          {
            "label": "United Kingdom",
            "value": "uk"
          }
        ]
      }
    },
    {
      "id": "medium-selected",
      "name": "Medium, with a choice and a description",
      "props": {
        "size": "medium",
        "label": "Time zone",
        "description": "Used for reminders and reports.",
        "options": [
          {
            "label": "Europe/Paris",
            "value": "paris",
            "selected": true
          },
          {
            "label": "Europe/London",
            "value": "london"
          },
          {
            "label": "America/New_York",
            "value": "new-york"
          }
        ]
      }
    },
    {
      "id": "small",
      "name": "Small",
      "props": {
        "size": "small",
        "label": "Sort by",
        "options": [
          {
            "label": "Relevance",
            "value": "relevance",
            "selected": true
          },
          {
            "label": "Newest first",
            "value": "date"
          },
          {
            "label": "Price",
            "value": "price"
          }
        ]
      }
    },
    {
      "id": "large",
      "name": "Large",
      "props": {
        "size": "large",
        "label": "Plan",
        "options": [
          {
            "label": "Free",
            "value": "free"
          },
          {
            "label": "Team",
            "value": "team",
            "selected": true
          },
          {
            "label": "Enterprise (contact sales)",
            "value": "enterprise",
            "disabled": true
          }
        ]
      }
    },
    {
      "id": "medium-invalid",
      "name": "Medium, required and invalid",
      "description": "No choice made in a required field; the error message says what to do.",
      "props": {
        "size": "medium",
        "label": "Country",
        "placeholder": "Choose a country",
        "required": true,
        "invalid": true,
        "errorMessage": "Choose a country.",
        "options": [
          {
            "label": "France",
            "value": "fr"
          },
          {
            "label": "Germany",
            "value": "de"
          }
        ]
      }
    },
    {
      "id": "medium-disabled",
      "name": "Medium, disabled",
      "props": {
        "size": "medium",
        "label": "Currency",
        "disabled": true,
        "options": [
          {
            "label": "Euro (EUR)",
            "value": "eur",
            "selected": true
          },
          {
            "label": "US dollar (USD)",
            "value": "usd"
          }
        ]
      }
    }
  ],
  "tokens": {
    "source": "packages/tokens/src/components/select.tokens.json",
    "prefix": "select"
  },
  "implementations": {
    "web": {
      "css": "packages/components/src/components/select/select.css",
      "template": "packages/components/src/components/select/select.template.mjs",
      "selector": ".ds-select",
      "element": "div",
      "attributes": {
        "size": "data-size",
        "disabled": "data-disabled",
        "invalid": "data-invalid"
      },
      "stateAttribute": "data-state",
      "rolePart": "control",
      "partSelectors": {
        "container": ".ds-select",
        "label": ".ds-select__label",
        "field": ".ds-select__field",
        "indicator": ".ds-select__indicator",
        "option": ".ds-select__option",
        "control": ".ds-select__control",
        "description": ".ds-select__description",
        "error-message": ".ds-select__error-message"
      }
    }
  },
  "related": [
    "radio-group",
    "dropdown",
    "text-field"
  ],
  "decisions": [
    {
      "id": "field-colors",
      "date": "2026-10-03",
      "version": "0.1.0",
      "area": "color",
      "status": "active",
      "text": "The control is a box on the default surface outlined in the accent neutral border color; hover tints it with the surface hover color and keeps the border; invalid takes the base error border; disabled takes the shared disabled colors. Value text is primary, the placeholder tertiary, the description secondary, the error message the error text color.",
      "rationale": "The Figma library maps no color to form controls. The accent neutral border is the lightest neutral border that keeps 3:1 against the surface in both modes (WCAG 1.4.11); the hover tint reuses the surface's own hover step.",
      "tokens": [
        "select.control.background.*",
        "select.control.border-color.*",
        "select.control.color.*"
      ],
      "refs": [
        "wcag:1.4.11"
      ]
    },
    {
      "id": "sizes",
      "date": "2026-10-03",
      "version": "0.1.0",
      "area": "size",
      "status": "active",
      "text": "Three sizes from the Figma library's space and body scales: small (32 px high, 8 px inline padding, 14 px text), medium (40, 12, 16, the default) and large (48, 16, 20); the border is 0.6 px (stroke-width.sm) and the corners 4 px (corner-radius.xs) at every size; label, description and error message keep 14 px.",
      "tokens": [
        "select.control.min-height.*",
        "select.control.padding-inline.*",
        "select.control.typography.*",
        "select.control.border-width",
        "select.control.border-radius"
      ]
    },
    {
      "id": "label-description-error",
      "date": "2026-10-03",
      "version": "0.1.0",
      "area": "anatomy",
      "status": "active",
      "text": "A field always has a visible label above the control; help text goes under the control as the description, and the error message under it while the field is invalid. Both are exposed as the control's description.",
      "refs": [
        "wcag:3.3.2",
        "wai-labels",
        "wai-instructions",
        "apg-describing"
      ]
    },
    {
      "id": "native-list",
      "date": "2026-10-03",
      "version": "0.1.0",
      "area": "behaviour",
      "status": "active",
      "text": "The select is the platform's native select: the component styles the closed box and its chevron only; the opened list, its keyboard interaction and its announcements belong to the browser or the platform and are not specified here.",
      "rationale": "A custom list needs script to be accessible (focus management, arrow keys, type-ahead), and the web implementation ships none.",
      "refs": [
        "html-aria-select",
        "html-select"
      ]
    },
    {
      "id": "chevron-indicator",
      "date": "2026-10-03",
      "version": "0.1.0",
      "area": "anatomy",
      "status": "active",
      "text": "A chevron (expand_more, the icon font) sits over the end edge of the control at 16, 20 or 24 px by size, 8 px from the edge; the control reserves 28, 36 or 40 px of end padding for it. It follows the value color and is hidden from assistive technology.",
      "tokens": [
        "select.indicator.*",
        "select.control.padding-inline-end.*"
      ],
      "refs": [
        "wcag:1.1.1",
        "aria-hidden"
      ]
    }
  ],
  "changelog": [
    {
      "version": "0.1.0",
      "date": "2026-10-03",
      "note": "Initial draft (decisions field-colors, sizes, label-description-error, native-list, chevron-indicator). Native select in a wrapper with a label element, a chevron, a description and an error message; options as a repeated part; states default, hover, focus-visible, invalid, disabled; sizes small, medium, large. Contrast on the default surface, light / dark: label and value 19.07 / 11.27, description 12.87 / 8.80, error text 4.52 / 4.97 (4.26 on level 1 in light mode), border at rest 12.87 / 8.80 and 12.14 / 8.06 on hover, error border 6.12 / 3.82, focus ring 5.79 / 12.80."
    }
  ]
}

Follows the Color foundation (13 tokens), the Focus ring foundation (3 tokens), the Iconography foundation (5 tokens), the Motion foundation (2 tokens), the Shape foundation (2 tokens), the Spacing and sizing foundation (14 tokens), the Typography foundation (6 tokens).

Anatomy

PartRoleRequiredDescriptionWeb selector
container container yes Root. Stacks the label, the control, the description and the error message; carries the size and the forced state. .ds-select
label label yes Text naming the field. Provides the accessible name; activating it focuses the control. .ds-select__label
field container yes Box holding the control and the chevron drawn over its end edge. .ds-select__field
control value yes The native select, drawn by the component as a closed box showing the chosen option. Receives focus, holds the value; the browser opens and renders the list. .ds-select__control
indicator (decorative) indicator yes Chevron glyph of the icon font at the end edge of the control, saying that it opens a list. Hidden from assistive technology, which learns it from the role. .ds-select__indicator
option (repeatable) value yes One choice in the list, rendered by the browser: the component gives it no style of its own. .ds-select__option
description description no Optional help text under the control (format, example, consequence), associated with it as its description. .ds-select__description
error-message errormessage no Text describing the error when the field is invalid, associated with the control as a description. Shown only while invalid.
Shown when invalid
.ds-select__error-message

Hover or focus a row to outline the part in the rendering above; hover the rendering to find its row.

Properties

PropertyTypeOptions / defaultBinds to / affectsDescription
size enum
  • small — Dense forms, toolbars, table rows (32 px high).
  • medium (default) — Default (40 px).
  • large — Prominent, single fields: a search page, a sign-in form (48 px).
affects controlindicator
web: data-size
Control height, inline padding and text size scale together; label, description and error keep their size.
label required string —
binds label
What the field asks for, as a noun phrase (Email address). Always visible.
options required list entries: option
binds option
The choices, in order.
placeholder string — Text shown while no option is chosen (Choose a country). A disabled, unselectable first option: never the label.
description string —
binds description
Help text: expected format, example, or what the value is used for.
errorMessage string —
binds error-message
What is wrong and how to fix it, shown when the field is invalid.
required boolean false The field must be filled to submit the form.
invalid boolean false
web: data-invalid
The value failed validation: the border takes the error color and the error message is shown.
disabled boolean false
web: data-disabled
Non-interactive and skipped by the keyboard; the value stays visible. Prefer read-only when people may need to read or copy the value.

Part properties

Set on each instance of a repeated part, through the list property holding it.

PropertyTypeOptions / defaultBinds to / affectsDescription
option.label required string — Text of the option, what people read and what is announced.
option.value string — Value submitted when the option is chosen. Defaults to the option's position.
option.selected boolean false Whether this option is the current choice.
option.disabled boolean false The option stays in the list, dimmed, and cannot be chosen.

States

StateTriggerApplies toDescription
default interaction every platform and input Resting: an outlined box on the default surface.
hover interaction input: pointer Pointer over the control. The box takes the surface hover tint; the border does not change. Needs a pointing device: touch has no hover.
focus-visible interaction input: keyboard Keyboard focus on the control. Shows the shared focus ring outside its border; pointer focus shows none.
invalid property (invalid) every platform and input Validation failed: the border takes the error color and the error message is shown under the control.
disabled property (disabled) every platform and input Non-interactive. Box, value, label and description drop to the shared disabled colors; hover and focus do not apply.

Layout

direction verticalalign stretchjustify startwrap falsesizing fill

  • Label, control, description and error message stack, separated by container.gap; the control fills the width of the component, which the layout sizes.
  • Inline padding, minimum height and text size come from the size option; the corner radius and the border width do not change.
  • The chevron sits over the end edge of the control, which reserves room for it with padding-inline-end.

Accessibility

Target: WCAG 2.2 level AA. Every statement cites the trusted source it comes from.

ARIA role combobox.

Accessible name: Computed from the label, associated with the control as its label element on the web. The description and the error message are exposed as the accessible description, not as part of the name. WAI tutorials: Labeling controlsHTML-AAM: accessible name computation of other form elements (select: the label element)APG: Describing with aria-describedby

KeyActionPlatformsSources
Tab Moves focus to the control. A disabled control is not focusable and is skipped. Web WCAG 2.1.1 Keyboard (A)HTML: focusable areaHTML: disabled form controls
Space, Enter, arrow keys Open the list and move through the options the way the platform's native select does; the browser owns this interaction. Web HTML: the select elementMDN: the select element (labeling, accessibility)

Every platform

Web

iOS

Android

WCAG success criteria

CriterionLevelUnderstanding
1.1.1 Non-text Content A Understanding 1.1.1
1.3.1 Info and Relationships A Understanding 1.3.1
1.4.1 Use of Color A Understanding 1.4.1
1.4.3 Contrast (Minimum) AA Understanding 1.4.3
1.4.11 Non-text Contrast AA Understanding 1.4.11
2.1.1 Keyboard A Understanding 2.1.1
2.3.3 Animation from Interactions AAA (beyond the AA target) Understanding 2.3.3
2.4.7 Focus Visible AA Understanding 2.4.7
2.4.13 Focus Appearance AAA (beyond the AA target) Understanding 2.4.13
2.5.8 Target Size (Minimum) AA Understanding 2.5.8
3.3.1 Error Identification A Understanding 3.3.1
3.3.2 Labels or Instructions A Understanding 3.3.2
4.1.2 Name, Role, Value A Understanding 4.1.2

References

Content

  • The label is a noun phrase naming the choice: Country, Time zone, Sort by. Sentence case, no colon, no trailing punctuation.
  • Option labels are short and parallel; order them logically (alphabetical for long lists, most common first for short ones).
  • The placeholder, when any, is an invitation to choose (Choose a country), shown until a choice is made; it is never a valid value.
  • The error message says what to do: Choose a country.

Changelog

  • 0.1.0 2026-10-03 — Initial draft (decisions field-colors, sizes, label-description-error, native-list, chevron-indicator). Native select in a wrapper with a label element, a chevron, a description and an error message; options as a repeated part; states default, hover, focus-visible, invalid, disabled; sizes small, medium, large. Contrast on the default surface, light / dark: label and value 19.07 / 11.27, description 12.87 / 8.80, error text 4.52 / 4.97 (4.26 on level 1 in light mode), border at rest 12.87 / 8.80 and 12.14 / 8.06 on hover, error border 6.12 / 3.82, focus ring 5.79 / 12.80.