<label class="field">
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<div class="list">
<option>Outlined (default)</option>
<option>Option Two</option>
<option>Option Three</option>
</div>
</select>
</label>
<label class="field filled">
<select>
<!-- -->
</select>
</label>
Components
Select
Leverages the List component to provide markup for the Select popover.
Non-experimental Select: Uses less modern features and the native option list.
Variants
Supporting text
.supporting-text
: supporting text element
<label class="field">
<span class="label">Label</span>
<select>
<!-- -->
</select>
<span class="supporting-text">Supporting text</span>
</label>
Validation
- Add
[required]
to the<select>
element to toggle required styles - The
.error
class toggles the error styles. Make use of the supporting text to give extra feedback on the error.
<label class="field">
<span class="label">Label</span>
<select required> <!----> </select>
</label>
<label class="field error">
<span class="label">Label</span>
<select> <!----> </select>
<span class="supporting-text">Supporting text</span>
</label>
Sizes
<label class="field small">
<!-- -->
</label>
Grouped
Wrap your options in an element with role="group"
. The <label>
inside will be used as a group heading.
<label class="field">
<span class="label">Small</span>
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<div class="list">
<option>Select car</option>
<div role="group">
<label class="text">Swedish cars</label>
<option>Volvo</option>
<option>SAAB</option>
</div>
<div role="group">
<label class="text">French cars</label>
<option>Renault</option>
<option>Citroën</option>
</div>
</div>
</select>
</label>
Dense
Since the Select popover uses the List component we can simply apply its .dense
modifier class.
<div class="field">
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<div class="list dense">
<option>Dense</option>
<option>Dense Two</option>
<option>Dense Three</option>
</div>
</select>
</div>
Validation
The .error
class toggles the error styles. Make use of the supporting text to give extra feedback on the error.
<label class="field error">
<span class="label">Label</span>
<select> <!-- --> </select>
<span class="supporting-text">Supporting text</span>
</label>
Non-experimental Select
Just implement the Select as you normally would.
<label class="field">
<span class="label">Label</span>
<select>
<option value="">-</option>
<option>Option 1</option>
<option>Option 2</option>
</select>
</label>
Anatomy
- Select container:
<select>
- Select button:
<button>
- Select button selected option:
<selectedcontent>
- Select button arrow
- Popover list:
.list
- List option/s:
<option>
- List option group/s (optional):
<optgroup>
Accessibility
Experimental status
This way of writing Selects are currently quite experimental. Accessible solutions are on the way, but not solved yet.
API
Field API
Type | Modifiers | Default | Description |
---|---|---|---|
Auto-fit | .auto-fit | - | When enabled, the element changes size depending on its content. |
Children | .label , <input> , <select> , <textarea> , <datalist> , .supporting-text | - | Optional children. |
Sizes | .small | - | The size of the element. |
Validation | .error | - | When applied, error styles are shown. |
Variants | outlined, .filled | outlined | The variant to use. |
List API
Type | Modifiers | Default | Description |
---|---|---|---|
Dense | ul.dense | - | When enabled list appears tighter packed |
Gutterless | ul.gutterless | - | When enabled list inline padding is removed |
Bordered | ul.bordered | - | When enabled a border is rendered on all list items |
List item
Type | Modifiers | Default | Description |
---|---|---|---|
Main parts | li > .start , li > .text , li > .end | - | Building blocks in List item |
Inset | li.inset | - | When enabled a list item without a start icon algins with items that do |
Border top | li.border-top | - | When enabled the list item will get a top border |
Browser compatibility
Experimental Web Platform features feature flag required
The Select makes use of the latest customizable select API which limits it to Chromium version 133<.
The non-experimental Select is usable today though and might work as a fallback while we wait for the browsers to catch up!
Installation
@layer components.has-deps {
/*
- Common styling for input, textarea and select
- Form related styles such as: label, supporting text, error handling
*/
:where(.field) {
--_accent-color: var(--primary);
--_bg-color: var(--surface-default);
--_border-color: var(--field-border-color);
--_field-padding-block: 0.75rem;
--_field-padding-inline: var(--size-2);
--_filled-border-color: var(--text-color-1);
--_height: var(--field-size);
--_label-color: var(--text-color-2);
--_supporting-text-color: var(--text-color-2);
display: inline-grid;
position: relative;
/* Input/Select base */
& input,
& textarea,
& select {
background-color: var(--_bg-color);
block-size: var(--_height);
border-radius: var(--field-border-radius);
border: var(--field-border-width) solid var(--_border-color);
color: var(--text-color-1);
font-family: var(--font-sans);
font-size: var(--font-size-1);
grid-column: 1/-1;
grid-row: 1;
inline-size: 100%;
line-height: var(--font-lineheight-1);
min-inline-size: 0;
padding: var(--_field-padding-block) var(--_field-padding-inline);
@media (prefers-reduced-motion: no-preference) {
transition:
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
padding-block 0.2s var(--ease-3);
}
}
/* Required/Invalid */
&:has(
:not(:placeholder-shown):invalid,
:where(
:placeholder-shown,
option[value=""]:not(:checked),
option:checked:not([value=""])
):required
) {
.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-color-1);
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);
border: none;
block-size: calc(100% - var(--size-2) * 2);
border-radius: var(--field-border-radius);
cursor: pointer;
margin-inline-end: 1ex;
margin-block-start: var(--size-2);
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-sm);
&::-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));
}
}
/* Experimental 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-experimental 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"]) {
input {
appearance: none;
background: none;
block-size: var(--_height);
overflow: hidden;
padding: 0;
&::-webkit-color-swatch {
border: none;
}
&::-webkit-color-swatch-wrapper {
padding: 0;
}
}
.label {
border: 1px solid var(--field-border-color);
inline-size: fit-content;
margin-inline-start: var(--size-2);
}
}
/* Textarea */
&:has(textarea) {
.label {
align-self: start;
margin-block-start: var(--_field-padding-block);
}
}
/*
* Variant: Outlined
*/
&:not(.filled) {
/* Element states */
&:hover {
&:not(.error) {
:where(input, textarea, select) {
--_border-color: var(--text-color-1);
}
}
}
}
&:not(.filled):focus-within {
& input,
& textarea,
& select {
border-color: var(--_accent-color);
outline-offset: -2px;
outline: 2px solid var(--_accent-color);
}
}
/* Label */
.label {
align-self: center;
background-color: var(--_bg-color);
border-radius: var(--field-border-radius);
color: var(--_label-color);
border-radius: var(--radius-1);
display: inline-flex;
font-size: var(--font-size-md);
grid-column: 1/-1;
grid-row: 1;
inline-size: calc(100% - (var(--field-border-width) * 2));
margin-inline-start: var(--field-border-width);
padding-inline: var(--_field-padding-inline);
pointer-events: none;
z-index: 1;
@media (prefers-reduced-motion: no-preference) {
transition:
border-color 0.2s var(--ease-3),
font-size 0.2s var(--ease-3),
inline-size 0.05s var(--ease-3),
margin 0.2s var(--ease-3),
padding-inline 0.2s var(--ease-3);
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:focus-within,
&:has(:where(input:not([type="color"]), textarea):not(:placeholder-shown)),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
.label {
border-color: transparent;
color: var(--_accent-color);
font-size: 0.75rem;
inline-size: max-content;
letter-spacing: 0.15px;
line-height: 1.15;
margin-block-start: -2.7rem;
margin-inline-start: var(--_field-padding-inline);
padding-inline: 0.125rem;
}
/* Neutral label color reset */
&:not(:focus-within):not(.error) {
.label {
color: var(--text-color-2);
}
}
&:has(textarea) {
.label {
margin-block-start: -0.35rem;
}
&.small {
.label {
align-self: start;
margin-block-start: var(--_field-padding-block);
}
}
}
}
/* Supporting text */
.supporting-text {
color: var(--_supporting-text-color);
font-size: var(--font-size-xs);
grid-row: 3;
line-height: 1.5;
margin-inline-start: var(--field-border-width);
padding-inline: var(--_field-padding-inline);
z-index: 1;
}
/* Auto-fit */
&.auto-fit {
inline-size: auto;
:where(& input, & textarea) {
field-sizing: content;
}
}
/* Validation */
&.error {
--_accent-color: var(--color-9);
--_border-color: var(--color-9);
--_filled-border-color: var(--color-9);
--_label-color: var(--color-9);
--_supporting-text-color: var(--color-9);
}
/*
* Variant: Filled
*/
&.filled {
--_bg-color: var(--surface-tonal);
*:focus-visible {
outline: 0;
}
/* Base style */
& input,
& textarea,
& select {
border-block-end-color: var(--_filled-border-color);
border-block-start-color: transparent;
border-inline-color: transparent;
border-radius: 0;
}
& input[type="color"] {
border-inline: none;
}
/* Bottom line */
&:before {
background-color: var(--_filled-border-color);
block-size: calc(var(--field-border-width) + 1px);
content: "";
inline-size: 100%;
margin-block-end: calc(-1 * (var(--field-border-width) * 2));
transform: scaleX(0);
translate: 0 calc(-1 * (var(--field-border-width) * 2));
z-index: 1;
@media (prefers-reduced-motion: no-preference) {
transition:
transform 0.3s var(--ease-3),
translate 0.2s var(--ease-3);
}
}
/* Label */
.label {
background-color: var(--_bg-color);
}
&: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)
);
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:has(.label) {
&:focus-within,
&:has(
:where(input:not(:where([type="color"])), textarea):not(
:placeholder-shown
)
),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
:where(input, textarea) {
padding-block: calc(var(--_field-padding-block) * 1.7)
calc(var(--_field-padding-block) * 0.3);
}
select > button,
select:not(:has(button)) {
padding-block: calc(var(--_field-padding-block) * 1.7)
calc(var(--_field-padding-block) * 0.3);
}
.label {
margin-block-start: calc(-1 * var(--size-5));
margin-inline-start: calc(var(--_field-padding-inline) / 2);
padding-inline: calc(var(--_field-padding-inline) / 2);
}
&:has(textarea) {
.label {
margin-block-start: var(--size-1);
}
}
}
}
/* Element states */
&:hover {
&:before {
transform: scaleX(1);
}
}
&:focus-within {
& input,
& textarea,
& select {
border-block-end-color: var(--_accent-color);
}
&:before {
background-color: var(--_accent-color);
transform: scaleX(1) translateX(0px);
}
}
}
/* Disabled */
&:where(:has([disabled])) {
&:before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
opacity: 0.7;
* {
pointer-events: none;
}
}
}
/* 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;
}
}
&:has(textarea) {
.label {
align-self: center;
margin-block-start: unset;
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:focus-within,
&:has(
:where(input:not([type="color"]), textarea):not(:placeholder-shown)
),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
.label {
margin-block-start: -2.2rem;
margin-inline-start: var(--size-1);
padding-inline: var(--size-1);
}
&:not(.filled):has(textarea) {
.label {
margin-block-start: -0.35rem;
}
}
}
}
}
}
@layer components.has-deps {
:where(.field > select) {
position: relative;
/* Default arrow */
&:after {
display: none;
}
&:open {
button {
&:after {
rotate: 180deg;
}
}
}
/* Select popover */
&::picker(select) {
border: 0;
box-shadow: var(--shadow-2);
padding: 0;
@media (prefers-reduced-motion: no-preference) {
transition:
opacity 0.5s var(--ease-3),
scale 0.2s var(--ease-3);
}
}
/* Animation start styles */
&:not(:open)::picker(select) {
opacity: 0;
scale: 0.5;
}
/* Animation end styles */
&:open::picker(select) {
opacity: 1;
scale: 1;
}
button {
background-color: transparent;
display: flex;
inline-size: 100%;
margin: 0;
position: relative;
/* Arrow */
&:after {
block-size: 0;
border-block-start: 5px solid;
border-inline: 5px solid transparent;
content: "";
display: inline-block;
flex-shrink: 0;
inline-size: 0;
inset: 50% var(--size-3) auto auto;
pointer-events: none;
position: absolute;
translate: 0 -50%;
}
selectedcontent {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.list {
/* Groups */
[role="group"] {
label {
background-color: light-dark(var(--gray-3), var(--gray-13));
color: light-dark(
oklch(from var(--text-color-1) calc(l * 0.75) c h),
oklch(from var(--text-color-1) 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(.field:has(> select)) {
/* Size */
&.small {
button {
padding-block: var(--size-1);
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 `.field` class instead. Noting this down if an `:after` element would be needed on a `.field`.
*/
&: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: 1;
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.has-deps {
/*
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: light-dark(var(--gray-1), var(--gray-15));
background-color: var(--_bg-color);
list-style: none;
padding: var(--size-2) 0;
@media (pointer: coarse) {
&,
* {
user-select: none;
}
}
/* 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;
}
}
/* Leading and trailing content */
.start,
.end {
.avatar {
max-inline-size: var(--size-6);
}
.icon-button,
svg {
max-inline-size: var(--size-4);
}
.checkbox,
.radio {
max-inline-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: var(--_bg-color) var(--ripple, none);
display: flex;
font-size: var(--font-size-sm);
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;
}
/* 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: light-dark(var(--gray-2), var(--gray-14));
}
&:checked {
background-color: oklch(from var(--primary) l c h / 30%);
}
}
& > a,
& > button,
& > label {
align-items: center;
background: var(--_bg-color) var(--ripple, none);
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;
/*** Ripple effect */
background-position: center;
transition: background var(--button-ripple-duration);
&:where(:not(:active):hover) {
--ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%)
center/15000%;
}
&:where(:hover:active) {
background-size: var(--button-ripple-size);
transition: background 0s;
}
&:hover {
background-color: light-dark(var(--gray-2), var(--gray-14));
}
/*** Remove ripple effect when trailing button is clicked */
&:has(.end:hover) {
&:where(:not(:active):hover) {
--ripple: none;
}
}
}
/* Checkbox / Radio / Switch */
& > label {
.end {
padding-inline-end: var(--size-1);
}
&:where(.checkbox, .radio) {
inline-size: 100%;
}
&.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 {
font-size: var(--font-size-xs);
}
}
/* Leading content */
.start {
align-self: center;
align-items: 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-xs);
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 {
max-inline-size: var(--size-5);
inline-size: 100%;
}
}
/* 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;
}
}
}
}
}