Navigation

v0.0.4
Forms

Select

A native select whose option list is drawn in the same material.

On this page

Use .select directly on a native <select>. The browser keeps the keys, type-ahead and the form value; the class draws the well and a quiet chevron that reads in both themes. Where the browser supports the customizable select, the option list is drawn in the same material as the rest of the interface.

Select

<label class="field">Framework  <select class="select">    <option class="select-option">Astro</option>    <option class="select-option">Next.js</option>    <option class="select-option">SvelteKit</option>    <option class="select-option" disabled>Nuxt</option>  </select></label>

#The option list

With appearance: base-select the list is no longer drawn by the operating system. It becomes ::picker(select), a popover that the browser anchors to the control and flips at the viewport edge, and the stylesheet gives it the floating sheet, whose corner is a row's corner plus the 4px between them. Put select-option on each <option>:

  • The row under the pointer or the arrow keys takes a chip tone.
  • The current option carries a check at its end, drawn on ::checkmark.
  • The control's chevron is ::picker-icon; it turns over while the list is open.

A browser without the customizable select ignores all of this and shows the platform's own list, as it always has. Nothing in the markup changes, and select-option does no harm there.

#Raised

In a toolbar or a filter row, select-raised makes the select a raised control beside buttons.

Raised

<select class="select select-raised select-sm" aria-label="Sort">  <option class="select-option">Newest first</option>  <option class="select-option">Oldest first</option>  <option class="select-option">Name</option></select><button type="button" class="btn btn-sm">Export</button>

#Sizes

A select is 32, 38 or 44px tall, as a button and an input are, whether it is a well or raised.

Sizes

<select class="select select-sm" aria-label="Small">  <option class="select-option">Small</option></select><select class="select" aria-label="Medium">  <option class="select-option">Medium</option></select><select class="select select-lg" aria-label="Large">  <option class="select-option">Large</option></select>

#Disabled

Disabled

<select class="select" aria-label="Region" disabled>  <option class="select-option">Asia Pacific</option></select>

#Classes

ClassDescription
selectA native select drawn as a well with a chevron
select-optionAn option in the customizable list: a row that takes a chip tone under the pointer or the keys, with a check on the current one
select-raisedA raised select for toolbars and filter rows, beside buttons; 38px tall unless a size is set. With select
select-sm32px tall; the default is 38px, a button's height. With select
select-lg44px tall; the default is 38px, a button's height. With select

#Source

@master/ui/components/select.css
/* * Select: a native <select> drawn as a well, or as a raised control in toolbars. * Where the browser supports appearance: base-select, the option list becomes a floating sheet in the same * material: options are rows with a concentric corner and the current one carries a check. Elsewhere the * select keeps the platform's own list. */@layer components {  .select {    --select-height: 2.375rem;    --select-padding-x: .75rem;    --select-font-size: .875rem;    --select-radius: var(--radius-control);    -webkit-tap-highlight-color: transparent;    /* A neutral mid-grey chevron reads on moonstone and obsidian alike. */    --select-indicator: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23888' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 6.5 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E");    min-width: 0;    min-height: var(--select-height);    max-width: 100%;    margin: 0;    padding-block: 0;    padding-inline: var(--select-padding-x) calc(var(--select-padding-x) + 1.25rem);    border: 0;    border-radius: calc(var(--select-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    background: var(--select-indicator) no-repeat right calc(var(--select-padding-x) - .125rem) center / 14px, var(--color-surface-inset);    color: var(--color-text-strong);    box-shadow: var(--shadow-pressed);    font: 400 var(--select-font-size)/1.45 var(--font-family-sans);    font-feature-settings: inherit;    text-overflow: ellipsis;    appearance: none;    cursor: pointer;    transition-property: box-shadow, background-color;    transition-duration: var(--duration-quick);    transition-timing-function: var(--easing-glide);    @media (hover: hover) {      &:hover:not(:focus-visible, :disabled) {        background-color: var(--color-surface-inset-hover);      }    }    /*     * Focused, or open. :open stays inside :is(), which forgives a browser that does not know it; a :not(:open)     * on the hover rule would drop that rule there. This rule matches hover's weight and follows it instead,     * so an open select keeps its raised face under the pointer.     */    &:is(:focus-visible, :open):not(:disabled) {      outline: none;      background-color: var(--color-surface-raised);      box-shadow: inset 0 0 0 .5px var(--color-line-mark), 0 0 0 .5px var(--color-line-mark), 0 0 0 3.5px var(--color-focus-halo);    }    &:disabled {      color: var(--color-text-disabled);      cursor: not-allowed;    }    /* The chevron stands at the end of the words, whichever way they run. */    &:dir(rtl) {      background-position: left calc(var(--select-padding-x) - .125rem) center, 0 0;    }    /* Raised: a control on a sheet, for toolbars and filters, matching the button beside it. */    &.select-raised {      background: var(--select-indicator) no-repeat right calc(var(--select-padding-x) - .125rem) center / 14px, linear-gradient(180deg, var(--color-surface-raised), var(--color-surface-raised-low)) var(--color-surface-raised-low);      box-shadow: var(--shadow-raised);      /* Its face is a gradient: swap backgrounds instantly, as a button does. */      transition-property: box-shadow;      font-weight: var(--font-weight-control);      @media (hover: hover) {        &:hover:not(:focus-visible, :disabled) {          box-shadow: var(--shadow-raised-hover);        }      }      &:is(:focus-visible, :open):not(:disabled) {        box-shadow: var(--shadow-raised);      }      &:focus-visible {        @apply --focus-ring;      }      &:disabled {        background-color: var(--color-surface-inset);        background-image: var(--select-indicator);        box-shadow: var(--shadow-flat);      }    }    /* A select is as tall as the button and the field beside it: 32, 38 and 44px. */    &.select-sm {      --select-height: 2rem;      --select-padding-x: .625rem;      --select-font-size: .8125rem;      --select-radius: var(--radius-control-sm);    }    &.select-lg {      --select-height: 2.75rem;      --select-padding-x: .875rem;      --select-font-size: 1rem;      --select-radius: var(--radius-control-lg);    }    @media (pointer: coarse) {      min-height: max(var(--select-height), 2.75rem);      font-size: max(var(--select-font-size), 1rem);    }    @media (forced-colors: active) {      outline: 1px solid FieldText;      outline-offset: -1px;      &:is(:focus-visible, :open) {        outline: 2px solid Highlight;        outline-offset: -2px;      }    }  }  /*   * The customizable select. The control becomes a flex row whose chevron is ::picker-icon, and the list is   * ::picker(select): a popover the browser anchors to the control and flips at the viewport edge.   */  @supports (appearance: base-select) {    .select {      display: inline-flex;      align-items: center;      justify-content: space-between;      gap: .5rem;      padding-block: 0;      padding-inline: var(--select-padding-x) calc(var(--select-padding-x) - .125rem);      background: var(--color-surface-inset);      appearance: base-select;      &::picker-icon {        content: '';        flex: none;        width: 14px;        height: 14px;        background: var(--color-text-muted);        mask: var(--select-indicator) center / contain no-repeat;        transition: rotate var(--duration-base) var(--easing-glide);      }      &:open::picker-icon {        rotate: x 180deg;      }      &:disabled::picker-icon {        background: var(--color-text-disabled);      }      &.select-raised {        @apply --surface-sheet;        &:disabled {          background: var(--color-surface-inset);        }      }      /* The list: a floating sheet 6px from the control, whose corner is a row's corner plus its 4px inset. */      &::picker(select) {        margin: 6px 0;        padding: 4px;        border: 0;        border-radius: calc(var(--radius-control-sm) * var(--corner-scale) + 4px);        corner-shape: var(--corner-shape);        background: var(--color-surface-floating);        color: var(--color-text-strong);        box-shadow: var(--shadow-floating);        font: 400 var(--select-font-size)/1.2 var(--font-family-sans);        scrollbar-width: thin;        appearance: base-select;        opacity: 0;        scale: .97;        transform-origin: top;        transition-property: opacity, scale, overlay, display;        transition-duration: var(--duration-quick);        transition-timing-function: var(--easing-glide);        transition-behavior: allow-discrete;      }      &:open::picker(select) {        opacity: 1;        scale: 1;        @starting-style {          opacity: 0;          scale: .97;        }      }      @media (prefers-reduced-motion: reduce) {        &::picker-icon {          transition: none;        }        &::picker(select), &:open::picker(select) {          scale: none;          @starting-style {            scale: none;          }        }      }      @media (forced-colors: active) {        &::picker-icon {          background: FieldText;        }        &::picker(select) {          border: 1px solid CanvasText;        }      }    }    /* An option in the list. The row under the pointer or the keys takes a chip tone; the current one a check. */    .select-option {      display: flex;      align-items: center;      gap: .5rem;      min-height: 2rem;      padding: 0 .5rem;      border-radius: calc(var(--radius-control-sm) * var(--corner-scale));      corner-shape: var(--corner-shape);      outline: none;      background: transparent;      color: var(--color-text-strong);      white-space: nowrap;      cursor: pointer;      &::checkmark {        content: '';        order: 1;        flex: none;        width: 14px;        height: 14px;        margin-inline-start: auto;        background: currentColor;        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-7'/%3E%3C/svg%3E") center / contain no-repeat;      }      &:focus-visible:not(:disabled) {        background: var(--color-surface-chip);      }      @media (hover: hover) {        &:hover:not(:disabled) {          background: var(--color-surface-chip);        }      }      /* Pressed: a step deeper than the pointer's tone. */      &:active:not(:disabled) {        background: var(--color-surface-inset-hover);      }      &:disabled {        color: var(--color-text-disabled);        cursor: not-allowed;      }      @media (pointer: coarse) {        min-height: 2.75rem;      }      @media (forced-colors: active) {        &:is(:hover, :focus-visible):not(:disabled) {          background: Highlight;          color: HighlightText;        }        &::checkmark {          background: CanvasText;        }      }    }  }}