Navigation

v0.0.4
Forms

Slider

A native range input: a thin well with a pearl thumb.

On this page

A slider picks a value from a range where the position matters more than the exact number: volume, opacity, zoom. Use .slider on a native <input type="range">. The browser keeps the keys, the form value and touch dragging; the class draws a thin well and the same pearl thumb a switch has.

Slider

<label class="field">Volume  <input type="range" class="slider" value="60"></label>

#The fill

The part of the track before the thumb fills with ink, without script. Firefox draws it natively. Elsewhere the stylesheet reads the thumb's position with a scroll-driven animation: the input is the scroll container the thumb is measured in, a view timeline on the thumb reports how far along it is, and the track paints up to that point. Where neither exists the track stays an empty well and the thumb still shows the value.

Steps and limits

<label class="field">Zoom  <input type="range" class="slider" min="50" max="200" step="25" value="100"></label><label class="field">Opacity  <input type="range" class="slider" value="35" disabled></label>

#Accessibility

  • Label the slider with a field label or aria-label.
  • A slider does not show its number. When the exact value matters, show it beside the slider or use an input with type="number".
  • On touch screens the thumb grows to 24px.

#Classes

ClassDescription
sliderA range input: a thin well with a pearl thumb, filled with ink before the thumb

#Custom properties

PropertyDefaultDescription
--slider-track.375remTrack thickness
--slider-thumb1.125remThumb diameter; 1.5rem on touch screens
--slider-colorvar(--color-surface-inverse)Fill color

#Source

@master/ui/components/slider.css
/* * Slider: a native range input: a thin well with a pearl thumb, the same thumb a switch has. * The part before the thumb fills with ink: Firefox draws it natively, and where scroll-driven animations * exist the thumb's own position in the track drives the fill, with no script. Elsewhere the track stays a well. */@property --slider-fill {  syntax: '<percentage>';  inherits: true;  initial-value: 0%;}@layer components {  .slider {    --slider-track: .375rem;    --slider-thumb: 1.125rem;    --slider-color: var(--color-surface-inverse);    /* Room around the thumb for its shadow and its focus ring; the negative margin keeps the track on the edge. */    --slider-bleed: 5px;    display: block;    box-sizing: content-box;    width: 100%;    height: var(--slider-thumb);    margin: calc(-1 * var(--slider-bleed));    padding: var(--slider-bleed);    border: 0;    background: transparent;    appearance: none;    cursor: pointer;    -webkit-tap-highlight-color: transparent;    &:focus-visible {      outline: none;    }    &::-webkit-slider-runnable-track {      height: var(--slider-track);      border-radius: 999px;      background: linear-gradient(90deg, var(--slider-color) var(--slider-fill), transparent 0) var(--color-surface-inset);      box-shadow: var(--shadow-pressed);    }    &::-webkit-slider-thumb {      width: var(--slider-thumb);      height: var(--slider-thumb);      margin-top: calc((var(--slider-track) - var(--slider-thumb)) / 2);      border: 0;      border-radius: 999px;      @apply --surface-thumb;      box-shadow: 0 0 0 .5px var(--color-line-strong), 0 1px 2px -.5px var(--color-shade-1), 0 2px 4px -1px var(--color-shade-2), 0 4px 8px -4px var(--color-shade-3);      appearance: none;      transition: box-shadow var(--duration-quick) var(--easing-glide), scale var(--duration-quick) var(--easing-glide);    }    &::-moz-range-track {      height: var(--slider-track);      border-radius: 999px;      background: var(--color-surface-inset);      box-shadow: var(--shadow-pressed);    }    &::-moz-range-progress {      height: var(--slider-track);      border-radius: 999px;      background: var(--slider-color);    }    &::-moz-range-thumb {      width: var(--slider-thumb);      height: var(--slider-thumb);      border: 0;      border-radius: 999px;      @apply --surface-thumb;      box-shadow: 0 0 0 .5px var(--color-line-strong), 0 1px 2px -.5px var(--color-shade-1), 0 2px 4px -1px var(--color-shade-2), 0 4px 8px -4px var(--color-shade-3);    }    /* Held, the thumb grows a little under the finger. */    &:active:not(:disabled)::-webkit-slider-thumb {      scale: 1.08;    }    &:focus-visible::-webkit-slider-thumb {      outline: 2px solid var(--color-focus);      outline-offset: 2px;    }    &:focus-visible::-moz-range-thumb {      outline: 2px solid var(--color-focus);      outline-offset: 2px;    }    &:disabled {      opacity: .5;      cursor: not-allowed;    }    @media (pointer: coarse) {      --slider-thumb: 1.5rem;    }    @media (prefers-reduced-motion: reduce) {      &:active:not(:disabled)::-webkit-slider-thumb {        scale: none;      }    }    @media (forced-colors: active) {      forced-color-adjust: none;      --slider-color: Highlight;      &::-webkit-slider-runnable-track {        background: linear-gradient(90deg, Highlight var(--slider-fill), transparent 0) Canvas;        box-shadow: inset 0 0 0 1px CanvasText;      }      &::-webkit-slider-thumb {        background: ButtonText;        box-shadow: none;      }      &::-moz-range-track {        background: Canvas;        box-shadow: inset 0 0 0 1px CanvasText;      }      &::-moz-range-thumb {        background: ButtonText;        box-shadow: none;      }    }  }  /*   * The fill. The input is the scroll container the thumb is measured in, so a view timeline on the thumb   * reads how far along the track it is; the input runs a one-property animation on that timeline and the   * track paints up to it. Clipping stays outside the bleed, so the thumb's shadow and focus ring are whole.   */  @supports (view-timeline: --slider-thumb inline) and (timeline-scope: --slider-thumb) {    .slider {      overflow: hidden;      timeline-scope: --slider-thumb;      /* Longhands: a shorthand would reset the timeline, and tools that fold one into it write a value no browser reads. */      animation-name: slider-fill;      animation-timing-function: linear;      animation-fill-mode: both;      animation-timeline: --slider-thumb;      animation-range: contain;      &::-webkit-slider-thumb {        view-timeline: --slider-thumb inline;        view-timeline-inset: var(--slider-bleed);      }      /* Right to left, the value grows toward the left: the reading and the paint both turn around. */      &:dir(rtl) {        animation-direction: reverse;      }      &:dir(rtl)::-webkit-slider-runnable-track {        background: linear-gradient(270deg, var(--slider-color) var(--slider-fill), transparent 0) var(--color-surface-inset);      }    }  }}@keyframes slider-fill {  from {    --slider-fill: 100%;  }  to {    --slider-fill: 0%;  }}