Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

Components

Select

Leverages the List component to provide markup for the Select popover.

Full support Supported since v135. Partial support Missing: customizable-select, overlay. Partial support Missing: customizable-select, overlay.

Variants

---
import { Select } from "@opui/astro"
---
<Select label="Label">
<option value="">-</option>
<option>Outlined (default)</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<Select label="Label" variant="filled">
<option value="">-</option>
<option>Filled</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<label class="select"> <span class="label" id="select-label-3">Label</span> <span class="field"> <select aria-labelledby="select-label-3" id="select-3"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Outlined (default)</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> </label> <label class="select filled"> <span class="label" id="select-label-4">Label</span> <span class="field"> <select aria-labelledby="select-label-4" id="select-4"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Filled</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> </label>

End text

.end-text: end text element

---
import { Select } from "@opui/astro"
---
<Select label="Label" endText="Supporting text">
<option value="">-</option>
<option>Outlined (default)</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<Select label="Label" variant="filled" endText="Supporting text">
<option value="">-</option>
<option>Filled</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<label class="select"> <span class="label" id="select-label-5">Label</span> <span class="field"> <select aria-labelledby="select-label-5" id="select-5"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Outlined (default)</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> <span id="end-text-1" class="end-text">Supporting text</span> </label> <label class="select filled"> <span class="label" id="select-label-6">Label</span> <span class="field"> <select aria-labelledby="select-label-6" id="select-6"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Filled</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> <span id="end-text-2" class="end-text">Supporting text</span> </label>

Affix

Use the prefix and suffix slots to affix icons or short text alongside the select inside the field's border.

---
import { Select } from "@opui/astro"
---
<Select label="Currency">
<Fragment slot="prefix">¢</Fragment>
<option value="">-</option>
<option>EUR</option>
<option>EUR</option>
<option>SEK</option>
</Select>
<Select label="Country">
<svg
slot="prefix"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M2 12h20"></path>
<path
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
></path>
</svg>
<option value="">-</option>
<option>Sweden</option>
<option>Norway</option>
<option>Denmark</option>
</Select>
<label class="select"> <span class="label" id="select-label-7">Currency</span> <span class="field"> <select aria-labelledby="select-label-7" id="select-7"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>EUR</option> <option>EUR</option> <option>SEK</option> </div> </select> <span class="prefix">¢</span> </span> </label> <label class="select"> <span class="label" id="select-label-8">Country</span> <span class="field"> <select aria-labelledby="select-label-8" id="select-8"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Sweden</option> <option>Norway</option> <option>Denmark</option> </div> </select> <span class="prefix"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg></span> </span> </label>

Validation

  • Add [required] to the <select> element to toggle required styles.
  • The critical prop toggles the error styles. Make use of the end text to give extra feedback on the error.
---
import { Select } from "@opui/astro"
---
<div class="example-row">
<Select label="Label" required>
<option value="">-</option>
<option>Pick me!</option>
<option>No me!!</option>
<option>Come on!</option>
</Select>
<Select label="Label" variant="filled" required>
<option value="">-</option>
<option>Pick me!</option>
<option>No me!!</option>
<option>Come on!</option>
</Select>
</div>
<div class="example-row">
<Select label="Label" critical endText="Supporting text">
<option value="">-</option>
<option selected>Wrong option</option>
<option>Also wrong!</option>
<option>Nothing's right!</option>
</Select>
<Select label="Label" variant="filled" critical endText="Supporting text">
<option value="">-</option>
<option selected>Wrong option</option>
<option>Also wrong!</option>
<option>Nothing's right!</option>
</Select>
</div>
<div class="example-row"> <label class="select"> <span class="label" id="select-label-9">Label</span> <span class="field"> <select aria-labelledby="select-label-9" id="select-9" required> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Pick me!</option> <option>No me!!</option> <option>Come on!</option> </div> </select> </span> </label> <label class="select filled"> <span class="label" id="select-label-10">Label</span> <span class="field"> <select aria-labelledby="select-label-10" id="select-10" required> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>Pick me!</option> <option>No me!!</option> <option>Come on!</option> </div> </select> </span> </label> </div> <div class="example-row"> <label class="select" data-invalid="true"> <span class="label" id="select-label-11">Label</span> <span class="field"> <select aria-labelledby="select-label-11" id="select-11"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option selected>Wrong option</option> <option>Also wrong!</option> <option>Nothing's right!</option> </div> </select> </span> <span id="end-text-3" class="end-text">Supporting text</span> </label> <label class="select filled" data-invalid="true"> <span class="label" id="select-label-12">Label</span> <span class="field"> <select aria-labelledby="select-label-12" id="select-12"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option selected>Wrong option</option> <option>Also wrong!</option> <option>Nothing's right!</option> </div> </select> </span> <span id="end-text-4" class="end-text">Supporting text</span> </label> </div>

Spread

Use the spread boolean prop to display the label and description on the left with the select on the right. The layout collapses to a column on narrow containers.

---
import { Select } from "@opui/astro"
---
<Select spread>
<Fragment slot="label">Country</Fragment>
<Fragment slot="description">Select your country of residence</Fragment>
<option value="">Select a country</option>
<option>Denmark</option>
<option>Finland</option>
<option>Iceland</option>
<option>Norway</option>
<option>Sweden</option>
</Select>
<Select spread variant="filled">
<Fragment slot="label">Language</Fragment>
<Fragment slot="description">Choose your preferred language</Fragment>
<Fragment slot="end-text">This affects UI translations</Fragment>
<option value="">Select a language</option>
<option>Danish</option>
<option>Finnish</option>
<option>Icelandic</option>
<option>Norwegian</option>
<option>Swedish</option>
</Select>
<Select spread required>
<Fragment slot="label">Required</Fragment>
<Fragment slot="description">You must select an option</Fragment>
<option value="">Select an option</option>
<option>Option 1</option>
<option>Option 2</option>
</Select>
<Select spread disabled>
<Fragment slot="label">Disabled</Fragment>
<Fragment slot="description">This select is disabled</Fragment>
<option>Option 1</option>
</Select>
<Select spread critical>
<Fragment slot="label">Invalid Select</Fragment>
<Fragment slot="description">This select has an error</Fragment>
<Fragment slot="end-text">Please select a valid option.</Fragment>
<option>Option 1</option>
</Select>
<Select spread>
<Fragment slot="label">Currency</Fragment>
<Fragment slot="description">Used for billing</Fragment>
<Fragment slot="prefix">¢</Fragment>
<option value="">-</option>
<option>EUR</option>
<option>EUR</option>
<option>SEK</option>
</Select>
<Select spread variant="filled">
<Fragment slot="label">Region</Fragment>
<Fragment slot="description">Affects data residency and latency</Fragment>
<Fragment slot="prefix">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M2 12h20"></path>
<path
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
></path>
</svg>
</Fragment>
<Fragment slot="end-text">Cannot be changed after deploy</Fragment>
<option value="">-</option>
<option>eu-north-1</option>
<option>us-east-1</option>
<option>ap-southeast-1</option>
</Select>
<label class="select spread"> <span class="label" id="select-label-13">Country</span> <span class="start-text">Select your country of residence</span> <span class="field"> <select id="select-13"> <button> <selectedcontent> </button> <div class="list"> <option value="">Select a country</option> <option>Denmark</option> <option>Finland</option> <option>Iceland</option> <option>Norway</option> <option>Sweden</option> </div> </select> </span> </label> <label class="select filled spread"> <span class="label" id="select-label-14">Language</span> <span class="start-text">Choose your preferred language</span> <span class="field"> <select id="select-14"> <button> <selectedcontent> </button> <div class="list"> <option value="">Select a language</option> <option>Danish</option> <option>Finnish</option> <option>Icelandic</option> <option>Norwegian</option> <option>Swedish</option> </div> </select> </span> <span id="end-text-5" class="end-text">This affects UI translations</span> </label> <label class="select spread"> <span class="label" id="select-label-15">Required</span> <span class="start-text">You must select an option</span> <span class="field"> <select id="select-15" required> <button> <selectedcontent> </button> <div class="list"> <option value="">Select an option</option> <option>Option 1</option> <option>Option 2</option> </div> </select> </span> </label> <label class="select spread"> <span class="label" id="select-label-16">Disabled</span> <span class="start-text">This select is disabled</span> <span class="field"> <select disabled id="select-16"> <button> <selectedcontent> </button> <div class="list"> <option>Option 1</option> </div> </select> </span> </label> <label class="select spread" data-invalid="true"> <span class="label" id="select-label-17">Invalid Select</span> <span class="start-text">This select has an error</span> <span class="field"> <select id="select-17"> <button> <selectedcontent> </button> <div class="list"> <option>Option 1</option> </div> </select> </span> <span id="end-text-6" class="end-text">Please select a valid option.</span> </label> <label class="select spread"> <span class="label" id="select-label-18">Currency</span> <span class="start-text">Used for billing</span> <span class="field"> <select id="select-18"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>EUR</option> <option>EUR</option> <option>SEK</option> </div> </select> <span class="prefix">¢</span> </span> </label> <label class="select filled spread"> <span class="label" id="select-label-19">Region</span> <span class="start-text">Affects data residency and latency</span> <span class="field"> <select id="select-19"> <button> <selectedcontent> </button> <div class="list"> <option value="">-</option> <option>eu-north-1</option> <option>us-east-1</option> <option>ap-southeast-1</option> </div> </select> <span class="prefix"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> </span> </span> <span id="end-text-7" class="end-text">Cannot be changed after deploy</span> </label>

Sizes

---
import { Select } from "@opui/astro"
---
<Select label="Small" size="small">
<option value="">Small</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<Select label="Default">
<option value="">Default</option>
<option>Option Two</option>
<option>Option Three</option>
</Select>
<label class="select small"> <span class="label" id="select-label-20">Small</span> <span class="field"> <select aria-labelledby="select-label-20" id="select-20"> <button> <selectedcontent> </button> <div class="list"> <option value="">Small</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> </label> <label class="select"> <span class="label" id="select-label-21">Default</span> <span class="field"> <select aria-labelledby="select-label-21" id="select-21"> <button> <selectedcontent> </button> <div class="list"> <option value="">Default</option> <option>Option Two</option> <option>Option Three</option> </div> </select> </span> </label>

Classic select

Bog-standard native HTML <select> without customized option list.

---
import { ClassicSelect } from "@opui/astro"
---
<ClassicSelect label="Label">
<option value="">-</option>
<option>Option</option>
<option>Option</option>
</ClassicSelect>
<ClassicSelect label="Label" variant="filled">
<option value="">-</option>
<option>Option 1</option>
<option>Option 2</option>
</ClassicSelect>
<label class="select">
<span class="label" id="select-label-22">Label</span>
<span class="field">
<select aria-labelledby="select-label-22" id="select-22">
<option value="">-</option>
<option>Option</option>
<option>Option</option>
</select>
</span>
</label>
<label class="select filled">
<span class="label" id="select-label-23">Label</span>
<span class="field">
<select aria-labelledby="select-label-23" id="select-23">
<option value="">-</option>
<option>Option 1</option>
<option>Option 2</option>
</select>
</span>
</label>

Anatomy

  1. Select container: <select>
  2. Select button: <button>
  3. Select button selected option: <selectedcontent>
  4. Select button arrow
  5. Popover list: .list
  6. List option/s: <option>
  7. List option group/s (optional): <optgroup>

API

Prop Type Default Description
dense boolean false Select dense state.
disabled boolean - Select disabled state.
description string - Description text displayed above the select.
endText string - Supporting text displayed below the select.
critical boolean - Select error state. Sets [data-invalid] on the root element.
items Item[] [] An array of objects with text and value properties.
label string - The label for the select.
required boolean - Select required state.
size "small" - The size of the select.
spread boolean false Spreads the label/description and select to opposite ends.
variant "outlined" | "filled" "outlined" The visual variant of the select.

Slots

Slot Description
default Alternative way to define options (using <option> elements).
label Slot for the label element.
description Slot for the description (start text) element, displayed above the field.
prefix Content placed at the inline-start of the field, inside the border.
suffix Content placed at the inline-end of the field, inside the border.
header Content placed above the select, inside the border, with a divider.
footer Content placed below the select, inside the border, with a divider.
end-text Slot for the supporting text (end text) element.

Classic Select API

Prop Type Default Description
disabled boolean - Select disabled state.
critical boolean - Select error state.
items Item[] [] An array of objects with text and value properties.
label string - The label for the select.
required boolean - Select required state.
size "small" - The size of the select.
variant "outlined" | "filled" "outlined" The visual variant of the select.

Browser support

Full support Supported since v135. Partial support Missing: customizable-select, overlay. Partial support Missing: customizable-select, overlay.

See also the full browser support guide.

Installation

Dependencies

@layer components.extended {
/*
- Common styling for input, textarea and select
- Form related styles such as: label, supporting text, error handling
*/
:where(.text-field, .textarea, .select) {
--_accent-color: var(--primary);
--_bg-color: var(--surface-default);
--_border-color: var(--field-border-color);
--_field-padding-block: var(--size-2);
--_field-padding-inline: var(--size-2);
--_filled-border-color: var(--text-primary);
--_height: var(--field-size);
--_label-color: var(--text-muted);
--_end-text-color: var(--text-muted);
display: grid;
gap: var(--size-1) 0;
position: relative;
/* Field - the actual input with a border around it */
.field {
background-color: var(--_bg-color);
border: var(--field-border-width) solid var(--_border-color);
border-radius: var(--field-border-radius);
display: grid;
grid-column: 1/-1;
grid-row: 2;
grid-template-columns: auto 1fr auto;
grid-template-areas:
"header header header"
"prefix input suffix"
"footer footer footer";
min-block-size: var(--_height);
@media (prefers-reduced-motion: no-preference) {
transition: border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
}
:where(.field) :where(input, textarea, select) {
background: transparent;
border: 0;
color: var(--text-primary);
font-family: var(--font-sans);
font-size: var(--font-size-1);
grid-area: input;
inline-size: 100%;
line-height: var(--font-lineheight-1);
min-inline-size: 0;
padding: var(--_field-padding-block) var(--_field-padding-inline);
&:focus,
&:focus-visible {
outline: 0;
}
}
/* Affixes */
.prefix,
.suffix {
align-items: center;
color: var(--_label-color);
display: inline-flex;
padding-inline: var(--_field-padding-inline);
white-space: nowrap;
}
.prefix {
grid-area: prefix;
}
.suffix {
grid-area: suffix;
}
.header,
.footer {
align-items: center;
color: var(--_label-color);
display: flex;
font-size: var(--font-size-0);
gap: var(--size-2);
padding: var(--size-1) var(--_field-padding-inline);
}
.header {
border-block-end: 1px solid var(--_border-color);
grid-area: header;
}
.footer {
border-block-start: 1px solid var(--_border-color);
grid-area: footer;
}
/* Remove padding on the input when affixes are next to it */
.field:has(> .prefix) :where(input, textarea, select) {
padding-inline-start: 0;
}
.field:has(> .suffix) :where(input, textarea, select) {
padding-inline-end: 0;
}
/* Textarea - keep affix aligned with the first line */
&.textarea .field {
.prefix,
.suffix {
align-self: start;
padding-block: var(--_field-padding-block);
}
}
/* Required/Invalid */
&:has(:invalid) {
.label:after {
color: var(--red);
content: "*";
margin: -0.25em auto auto 0.25em;
}
}
/* File */
&:has(input[type="file"]) {
cursor: pointer;
input {
align-self: flex-start;
block-size: var(--_height);
box-shadow: none;
color: var(--text-primary);
cursor: inherit;
max-inline-size: 100%;
padding: 0;
transition: font-size 0.2s var(--ease-3);
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-tonal);
block-size: calc(100% - var(--size-2) * 2);
border: none;
border-radius: var(--field-border-radius);
cursor: pointer;
margin-block-start: var(--size-2);
margin-inline-end: 1ex;
margin-inline-start: var(--size-2);
}
}
/* Variants */
&.filled {
input {
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-default);
block-size: calc(100% - var(--size-2) * 2);
border-radius: var(--field-border-radius);
cursor: pointer;
margin-block-start: var(--size-2);
}
}
}
/* Sizes */
&.small {
input {
font-size: var(--font-size-05);
&::-webkit-file-upload-button,
&::file-selector-button {
block-size: calc(100% - var(--size-2));
margin-block-start: var(--size-1);
}
}
}
}
/* Autosuggest */
&:has(input[list]) {
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Select */
&:has(select) {
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Customizable Select */
&:has(select button) {
select {
padding: 0;
button {
outline: 0;
padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline);
}
}
}
/* Non-customizable Select */
&:has(select):not(:has(button)) {
select {
padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline);
}
}
/* Input - color */
&:has(input[type="color"]) {
.field {
block-size: var(--_height);
inline-size: var(--_height);
min-block-size: 0;
overflow: hidden;
}
input {
appearance: none;
background: none;
block-size: var(--_height);
inline-size: var(--_height);
overflow: hidden;
padding: 0;
&::-webkit-color-swatch {
border: none;
}
&::-webkit-color-swatch-wrapper {
padding: 0;
}
}
}
/* Element states */
&:hover {
&:not([data-invalid]) {
.field {
border-color: var(--text-primary);
}
}
}
&:focus-within {
.field {
border-color: var(--_accent-color);
outline-offset: -2px;
}
}
/* Label */
.label {
color: var(--text-primary);
font-size: var(--font-size-05);
font-weight: 600;
grid-column: 1/-1;
grid-row: 1;
padding-inline: 0 1ex;
}
/* End text */
:where(.end-text) {
color: var(--_end-text-color);
font-size: var(--font-size-0);
grid-column: 1/-1;
grid-row: 3;
line-height: 1.5;
}
/* Auto-fit */
&.auto-fit {
inline-size: auto;
.field {
inline-size: auto;
}
:where(& input, & textarea) {
field-sizing: content;
inline-size: auto;
min-inline-size: 25ch;
}
}
/* Validation */
&[data-invalid],
&:has(:user-invalid) {
--_accent-color: var(--color-9);
--_border-color: var(--color-9);
--_filled-border-color: var(--color-9);
--_label-color: var(--color-9);
--_end-text-color: var(--color-9);
}
/*
* Variant: Filled
*/
&.filled {
--_bg-color: var(--surface-tonal);
&:not(:has([disabled], :has(input[type="color"]))) {
/* Hover */
&:hover {
--_bg-color: light-dark(oklch(from var(--surface-tonal) calc(l * 0.93) c h),
oklch(from var(--surface-tonal) calc(l * 1.1) c h));
}
}
}
/* Disabled */
&:where(:has([disabled])) {
cursor: not-allowed;
opacity: 0.64;
user-select: none;
&::before {
display: none;
}
.field {
cursor: not-allowed;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
.label,
.start-text,
.end-text {
cursor: not-allowed;
}
}
/* Read-only */
&:where(:has([readonly])) {
&::before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
}
/* Sizes */
&.small {
--_field-padding-block: var(--size-2);
--_height: var(--field-size-small);
&:has(input[type="color"]) {
.label {
line-height: 1.5;
}
}
}
/* Orientation */
&.spread {
align-items: start;
column-gap: var(--size-4);
container-type: inline-size;
grid-template-columns: 1fr auto;
.label {
font-weight: 600;
grid-column: 1;
grid-row: 1;
}
.start-text {
color: var(--_end-text-color);
font-size: var(--font-size-0);
grid-column: 1;
grid-row: 2;
line-height: 1.5;
}
.field {
align-self: start;
grid-column: 2;
grid-row: 1 / span 2;
}
.field:has(textarea) {
grid-row: 1 / span 3;
+.end-text {
grid-row: 4;
}
}
:where(textarea) {
min-inline-size: 30ch;
}
:where(.end-text) {
grid-column: 2;
text-align: end;
}
/* Collapse to column on narrow containers */
@container (width < 400px) {
column-gap: 0;
grid-template-columns: 1fr;
.label {
grid-column: 1;
grid-row: 1;
}
.start-text {
grid-column: 1;
grid-row: 2;
}
.field,
.field:has(textarea) {
grid-column: 1;
grid-row: 3;
}
:where(.end-text) {
grid-column: 1;
grid-row: 4;
text-align: start;
}
}
}
}
}
@layer components.extended {
:where(.select select) {
position: relative;
/* Default arrow */
&::picker-icon {
block-size: 0;
border-block-start: 5px solid;
border-inline: 5px solid transparent;
color: currentColor;
content: "";
inline-size: 0;
inset: 50% var(--size-3) auto auto;
pointer-events: none;
position: absolute;
translate: 0 -50%;
}
&:open {
&::picker-icon {
rotate: 180deg;
}
}
/* Select popover */
&::picker(select) {
/* Animation on-stage styles */
border: 0;
box-shadow: var(--shadow-2);
opacity: 1;
padding: 0;
scale: 1;
/* Animation starting styles */
@starting-style {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-reduced-motion: no-preference) {
transition: display 0.2s allow-discrete, opacity 0.2s var(--ease-3),
overlay 0.2s allow-discrete, scale 0.2s var(--ease-3);
}
}
/* Animation off-stage styles */
&:not(:open)::picker(select) {
opacity: 0;
scale: 0.9;
}
button {
align-items: center;
background-color: transparent;
display: flex;
inline-size: 100%;
margin: 0;
position: relative;
selectedcontent {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.list {
--_bg-color: var(--surface-filled);
border: var(--field-border-width) solid var(--field-border-color);
border-radius: var(--field-border-radius);
/* Groups */
[role="group"] {
label {
background-color: light-dark(var(--gray-3), var(--gray-13));
color: light-dark(oklch(from var(--text-primary) calc(l * 0.75) c h),
oklch(from var(--text-primary) calc(l * 1.25) c h));
font-weight: 500;
overflow: hidden;
padding-inline: var(--size-2);
text-overflow: ellipsis;
white-space: nowrap;
}
&:not(:first-child),
option:first-of-type {
margin-block-start: var(--size-2);
}
option:last-of-type,
&:last-child {
option:last-of-type {
margin-block-end: 0;
}
}
}
/* Option */
option {
/* Checkmark */
/* TODO - checkmark should be the final version of the checkmark API. Follow the development of this and remove redundant psuedo stuff. */
&::check {
display: none;
}
&::checkmark {
display: none;
}
&::before {
display: none;
}
&:focus-visible {
outline-offset: -1px;
}
}
}
}
:where(.select:has(select)) {
/* Size */
&.small {
button {
padding-inline: var(--size-2) var(--size-7);
&::after {
inset-inline-end: var(--size-2);
}
}
}
/*
* Non-experimental Select
*
* Hack to get the arrow working. Pseudo elements aren't allowed on the `<select>` element, so need to add it on the `.select` class instead. Noting this down if an `:after` element would be needed on a `.select`.
*/
&:not(:has(button)) {
select {
appearance: none;
}
/* Arrow */
&::after {
align-self: center;
block-size: 0;
border-block-start: 5px solid;
border-inline: 5px solid transparent;
content: "";
display: inline-block;
flex-shrink: 0;
grid-column: 1/-1;
grid-row: 2;
inline-size: 0;
inset-inline-end: var(--size-3);
justify-self: end;
pointer-events: none;
position: relative;
}
}
}
select:has(button),
::picker(select) {
appearance: base-select;
}
}
@layer components.extended {
/*
Lists meant to be used stand-alone or as part of Select elements
Intended use-case:
- ul.list > li
- .select > .list > option
*/
:where(.list) {
--_bg-color: var(--surface-filled);
--_bg-color-hover: oklch(from var(--primary) l c h / 15%);
background-color: var(--_bg-color);
color: var(--text-primary);
list-style: none;
padding: var(--size-2) 0;
@media (pointer: coarse) {
&,
* {
user-select: none;
}
}
/* Background color */
&.transparent {
--_bg-color: transparent;
}
&.default {
--_bg-color: var(--surface-default);
}
&.tonal {
--_bg-color: var(--surface-tonal);
}
/* Borders on all list items */
&.bordered {
:where(li + li, option + option) {
margin-block-start: var(--size-3);
&::before {
block-size: 1px;
border-block-start: 1px solid var(--border-color);
content: "";
display: block;
inline-size: 100%;
inset: calc(-1 * var(--size-2)) 0 auto 0;
position: absolute;
visibility: visible;
/* override select > option:before style */
}
}
}
/* Dense - less gaps and spacing */
&.dense {
:where(li, option) {
gap: var(--size-2);
min-block-size: var(--size-7);
padding: var(--size-1) var(--size-2);
&.border-top {
margin-block-start: var(--size-2);
&::before {
inset: calc(-1 * var(--size-1)) 0 auto 0;
}
}
/* Clickable list item */
&:has(> a, > button, > label) {
min-block-size: auto;
padding: 0;
}
&> :where(a, button, label) {
gap: var(--size-2);
min-block-size: var(--size-7);
padding: var(--size-1) var(--size-2);
}
/* Checkbox / Radio */
&>label {
.end {
padding-inline-end: 0.125rem;
}
&:where(.checkbox, .radio) {
--_input-size: var(--size-3);
}
}
/* Leading and trailing content */
.start,
.end {
.avatar {
max-inline-size: var(--size-6);
}
.icon-button,
svg {
max-inline-size: var(--size-4);
}
.checkbox,
.radio {
--_input-size: var(--size-3);
}
}
}
}
/* Gutterless */
&.gutterless {
:where(li, option) {
padding-inline: 0;
&> :where(a, button, label) {
padding-inline: 0;
}
}
}
/* List item */
:where(li, option, [role="group"] > label) {
align-items: center;
background-color: var(--_bg-color);
display: flex;
font-size: var(--font-size-05);
gap: var(--size-3);
isolation: isolate;
min-block-size: 40px;
padding: var(--size-2) var(--size-3);
position: relative;
&::before {
display: none;
/* removing checkmark from option */
}
* {
font-size: inherit;
}
/* Clickable list item */
&:has(> a, > button, > label) {
background: transparent;
display: block;
min-block-size: auto;
padding: 0;
&[aria-selected="true"]> :where(a, button, label),
&[aria-selected="true"],
&:has(> [aria-current="page"])> :where(a, button, label) {
background-color: var(--_bg-color-hover);
}
}
/* Select option */
&:where(option) {
align-items: center;
background-color: var(--_bg-color);
color: inherit;
cursor: pointer;
display: flex;
gap: var(--size-3);
inline-size: 100%;
margin: 0;
min-block-size: 40px;
padding: var(--size-2) var(--size-3);
text-align: start;
text-decoration: none;
z-index: 0;
&:hover {
background-color: var(--_bg-color-hover);
}
&[aria-selected="true"] {
background-color: var(--_bg-color-hover);
color: var(--primary);
}
&:checked {
background-color: oklch(from var(--primary) l c h / 30%);
}
}
&>a,
&>button,
&>label {
align-items: center;
background-color: var(--_bg-color);
color: inherit;
cursor: pointer;
display: flex;
gap: var(--size-3);
inline-size: 100%;
margin: 0;
min-block-size: 40px;
outline-offset: -3px;
padding: var(--size-2) var(--size-3);
text-align: start;
text-decoration: none;
z-index: 0;
&:hover {
background-color: var(--_bg-color-hover);
}
&[aria-selected="true"],
&[aria-current="page"] {
background-color: var(--_bg-color-hover);
}
}
/* Checkbox / Radio / Switch */
&>label {
&:where(.checkbox, .radio, .switch) {
display: flex;
}
.end {
padding-inline-end: var(--size-1);
}
&:where(.checkbox, .radio) {
inline-size: 100%;
}
/* Switches look comically big in lists, so it's better to use a smaller variant */
&.switch {
--_dot-size: 0.75rem;
--_track-height: var(--size-4);
--_track-width: 2.5rem;
}
}
/* Video */
&:has(video) {
padding: 0.75rem var(--size-3) 0.75rem 0;
}
/* Border between list items */
&.border-top {
margin-block-start: var(--size-3);
&::before {
block-size: 1px;
border-block-start: 1px solid var(--border-color);
content: "";
display: block;
inline-size: 100%;
inset: calc(-1 * var(--size-2)) 0 auto 0;
position: absolute;
}
}
/* Text */
.text {
flex: 1;
line-height: 1.6;
:where(h1, h2, h3, h4, h5, h6, p, span) {
color: inherit;
font-weight: 400;
}
p+p {
color: var(--text-muted);
font-size: var(--font-size-0);
}
}
/* Leading content */
.start {
align-items: center;
align-self: center;
display: grid;
z-index: 1;
&:has(svg) {
max-inline-size: var(--size-5);
}
svg {
padding-block-start: 0.125rem;
}
img {
aspect-ratio: 1;
inline-size: 56px;
object-fit: cover;
}
video {
aspect-ratio: 16/9;
block-size: 64px;
object-fit: cover;
}
}
/* Trailing content */
.end {
align-items: center;
display: flex;
font-size: var(--font-size-0);
text-align: end;
z-index: 1;
&:not(:has(a, button, input)) {
pointer-events: none;
}
kbd {
background-color: transparent;
border: 0;
color: inherit;
opacity: 0.6;
}
svg {
inline-size: 100%;
max-inline-size: var(--size-5);
}
}
/* Inset */
&.inset {
.text {
padding-inline-start: calc(var(--size-5) + var(--size-3));
}
/* Safety measure so it won't look bad if there for some reason should exist a leading element inside. */
.start {
display: none;
}
}
}
}
}

See also