CSS Range Slider: 16 Styled Examples for Every Browser

A CSS range slider restyles input type="range", the native control for picking a number between a minimum and a maximum. The browser handles dragging and keys, while CSS repaints the thumb, the track and sometimes the fill.

Below are 16 CSS range slider examples you can try and copy, followed by a guide to pseudo-elements, filled tracks, two-handle ranges and accessibility.

By Bogdan Sandu · Updated October 28, 2026

Styled native range sliders

Four ways to restyle a plain input type="range", from a single accent-color line to a fully custom thumb and track. Every one keeps the native keyboard support: focus a slider and try the arrow keys, Home and End.

Filled track with a custom property

  • --custom property
  • hard-stop gradient
  • ::-moz-range-progress
$1,800

Drag the thumb or focus it and press the arrow keys.

A five-line script writes the thumb position as a fraction to --rs-bud-v, and the WebKit track paints two gradient stops at the same spot, so the fill ends exactly under the thumb center. Firefox uses its native ::-moz-range-progress instead. The stop offset of half a thumb width is what keeps the fill aligned at both ends.

View code
.rs-bud {
  box-sizing: border-box;
  width: min(100%, 380px);
  padding: 24px;
  border-radius: 16px;
  background: #fff;
  color: #17201c;
  box-shadow: 0 12px 32px rgba(23, 32, 28, .08);
  font: inherit;
}
.rs-bud__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
  margin-bottom: 12px;
}
.rs-bud__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-bud__out {
  font-size: 24px;
  font-weight: 700;
  color: #1f6f5c;
  font-variant-numeric: tabular-nums;
}
.rs-bud__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-bud__range::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(to right, #1f6f5c calc(11px + var(--rs-bud-v) * (100% - 22px)), #7f8f88 0);
}
.rs-bud__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  margin-top: -7px;
  border: 4px solid #1f6f5c;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(23, 32, 28, .3);
}
.rs-bud__range::-moz-range-track {
  height: 8px;
  border-radius: 4px;
  background: #7f8f88;
}
.rs-bud__range::-moz-range-progress {
  height: 8px;
  border-radius: 4px;
  background: #1f6f5c;
}
.rs-bud__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  border: 4px solid #1f6f5c;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(23, 32, 28, .3);
}
.rs-bud__range:focus-visible {
  outline: 3px solid #1f6f5c;
  outline-offset: 2px;
  border-radius: 8px;
}
.rs-bud__scale {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-size: 13px;
  color: #56635d;
}

Script-free fill with a thumb shadow

  • box-shadow spread
  • overflow: hidden
  • step="5"
80%

Charging pauses at 80% and resumes when the level drops below it.

Drag the thumb or focus it and press the arrow keys.

The fill needs no custom property here. A huge box-shadow on the thumb, offset to the left by its own spread, paints everything behind the thumb, and overflow: hidden on the input clips it to the battery body. The trick only suits tracks as tall as the thumb, and the small script only updates the text.

View code
.rs-bat {
  box-sizing: border-box;
  width: min(100%, 360px);
  padding: 24px;
  border-radius: 18px;
  background: #fbf8f2;
  color: #1d1d1b;
  box-shadow: 0 1px 0 #e6dfd1, 0 14px 30px rgba(29, 29, 27, .08);
  font: inherit;
}
.rs-bat__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.rs-bat__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-bat__out {
  font-size: 24px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.rs-bat__cell {
  position: relative;
  margin-right: 10px;
  padding: 4px;
  border: 3px solid #1d1d1b;
  border-radius: 12px;
}
.rs-bat__cell::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -10px;
  width: 6px;
  height: 18px;
  margin-top: -9px;
  border-radius: 0 4px 4px 0;
  background: #1d1d1b;
}
.rs-bat__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 40px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  border-radius: 7px;
  background: #e5dccb;
  cursor: pointer;
}
.rs-bat__range::-webkit-slider-runnable-track {
  height: 40px;
  background: transparent;
}
.rs-bat__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 8px;
  height: 40px;
  border: 0;
  border-radius: 0;
  background: #1d1d1b;
  box-shadow: -1000px 0 0 1000px #3b8f57;
}
.rs-bat__range::-moz-range-track {
  height: 40px;
  background: transparent;
}
.rs-bat__range::-moz-range-progress {
  height: 40px;
  background: #3b8f57;
}
.rs-bat__range::-moz-range-thumb {
  width: 8px;
  height: 40px;
  border: 0;
  border-radius: 0;
  background: #1d1d1b;
}
.rs-bat__cell:has(.rs-bat__range:focus-visible) {
  outline: 3px solid #3b8f57;
  outline-offset: 2px;
}
.rs-bat__range:focus-visible {
  outline: none;
}
.rs-bat__hint {
  margin: 16px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: #57534a;
}
.rs-bat__num {
  font-weight: 700;
  color: #1d1d1b;
}

The accent-color one-liner

  • accent-color
  • inherited property
  • CSS filter
Pine-covered cliffs and terraced hillside above a calm sea in evening light
+10
+15
+30

Drag the thumb or focus it and press the arrow keys.

One declaration on the card, accent-color: #c2410c, tints all three native sliders and their filled part. Height, thumb size and shape stay with the browser, which is the trade-off. The sliders drive brightness, contrast and sepia filters on the photo through three custom properties.

View code
.rs-acc {
  box-sizing: border-box;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: 20px;
  align-items: center;
  width: min(100%, 620px);
  padding: 18px;
  border-radius: 16px;
  background: #fff;
  color: #1c1917;
  box-shadow: 0 12px 32px rgba(28, 25, 23, .08);
  font: inherit;
  accent-color: #c2410c;
}
.rs-acc__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 10px;
  filter: brightness(calc(1 + var(--rs-acc-e) / 100)) contrast(calc(1 + var(--rs-acc-c) / 100)) sepia(calc(var(--rs-acc-w) / 100));
}
.rs-acc__controls {
  display: grid;
  gap: 14px;
}
.rs-acc__row {
  display: grid;
  grid-template-columns: 1fr auto;
  row-gap: 2px;
}
.rs-acc__label {
  font-size: 14px;
  font-weight: 600;
}
.rs-acc__out {
  font-size: 14px;
  font-weight: 600;
  color: #9a3412;
  font-variant-numeric: tabular-nums;
}
.rs-acc__range {
  grid-column: 1 / -1;
  width: 100%;
  margin: 4px 0 0;
  cursor: pointer;
}

Value bubble above the thumb

  • <output>
  • calc() positioning
  • translateX(-50%)
12 seats

Billed monthly: $108 at $9 per seat

Drag the thumb or focus it and press the arrow keys.

The output element sits above the track and reuses the same fraction as the fill: left is half a thumb width plus the fraction times the usable width, so the bubble arrow always points at the thumb center. Because output is tied to the slider with for, the value has a semantic source instead of being a loose span.

View code
.rs-tm {
  box-sizing: border-box;
  width: min(100%, 400px);
  padding: 24px 28px;
  border-radius: 16px;
  background: #fff;
  color: #241f45;
  box-shadow: 0 12px 32px rgba(36, 31, 69, .1);
  font: inherit;
}
.rs-tm__label {
  display: block;
  font-size: 15px;
  font-weight: 600;
}
.rs-tm__field {
  position: relative;
  padding-top: 52px;
}
.rs-tm__bubble {
  position: absolute;
  top: 8px;
  left: calc(13px + var(--rs-tm-v) * (100% - 26px));
  transform: translateX(-50%);
  padding: 6px 10px;
  border-radius: 8px;
  background: #241f45;
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.rs-tm__bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -6px;
  border: 6px solid transparent;
  border-top-color: #241f45;
}
.rs-tm__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-tm__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background: linear-gradient(to right, #6247e8 calc(13px + var(--rs-tm-v) * (100% - 26px)), #8e8aa6 0);
}
.rs-tm__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 26px;
  height: 26px;
  margin-top: -10px;
  border: 0;
  border-radius: 8px;
  background: #241f45;
  box-shadow: inset 0 0 0 6px #241f45, inset 0 0 0 9px #fff;
}
.rs-tm__range::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background: #8e8aa6;
}
.rs-tm__range::-moz-range-progress {
  height: 6px;
  border-radius: 3px;
  background: #6247e8;
}
.rs-tm__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 8px;
  background: #241f45;
  box-shadow: inset 0 0 0 6px #241f45, inset 0 0 0 9px #fff;
}
.rs-tm__range:focus-visible {
  outline: 3px solid #6247e8;
  outline-offset: 2px;
  border-radius: 8px;
}
.rs-tm__total {
  margin: 16px 0 0;
  font-size: 14px;
  color: #4a4566;
}
.rs-tm__sum {
  font-size: 18px;
  color: #241f45;
}

Ticks, steps and vertical sliders

Sliders where the scale matters as much as the thumb: datalist tick marks, named steps, a five-point rating and an equalizer built from vertical inputs. When the raw number means little, aria-valuetext gives screen readers the readable version.

Tick marks from a datalist

  • list + <datalist>
  • display: flex on datalist
  • aria-valuetext
1.25×

Drag the thumb or focus it and press the arrow keys.

The input points at a datalist with list, and the datalist itself is turned into the visible scale: display: flex, padding of half a thumb on each side, and zero-width options that center their tick and label. The ticks stay aligned with the thumb because they use the same travel distance. Screen readers hear 1.25 times normal speed instead of 1.25.

View code
.rs-spd {
  box-sizing: border-box;
  width: min(100%, 400px);
  padding: 22px 24px 18px;
  border-radius: 14px;
  background: #fff;
  color: #1e293b;
  box-shadow: 0 0 0 1px #e2e8f0, 0 10px 28px rgba(30, 41, 59, .08);
  font: inherit;
}
.rs-spd__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.rs-spd__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-spd__out {
  font-size: 22px;
  font-weight: 700;
  color: #0e7490;
  font-variant-numeric: tabular-nums;
}
.rs-spd__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 30px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-spd__range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(to right, #0e7490 calc(10px + var(--rs-spd-v) * (100% - 20px)), #8193a8 0);
}
.rs-spd__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  margin-top: -8px;
  border: 3px solid #fff;
  border-radius: 50%;
  background: #0e7490;
  box-shadow: 0 0 0 1px #0e7490, 0 2px 5px rgba(30, 41, 59, .3);
}
.rs-spd__range::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: #8193a8;
}
.rs-spd__range::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: #0e7490;
}
.rs-spd__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  border: 3px solid #fff;
  border-radius: 50%;
  background: #0e7490;
  box-shadow: 0 0 0 1px #0e7490, 0 2px 5px rgba(30, 41, 59, .3);
}
.rs-spd__range:focus-visible {
  outline: 3px solid #0e7490;
  outline-offset: 2px;
  border-radius: 6px;
}
.rs-spd__ticks {
  display: flex;
  justify-content: space-between;
  padding: 0 10px;
}
.rs-spd__ticks option {
  position: relative;
  display: flex;
  justify-content: center;
  box-sizing: border-box;
  width: 0;
  min-height: 0;
  margin: 0;
  padding: 16px 0 0;
  color: #475569;
  font-size: 13px;
  line-height: 1.2;
  white-space: nowrap;
}
.rs-spd__ticks option::before {
  content: "";
  position: absolute;
  top: 2px;
  left: -1px;
  width: 2px;
  height: 8px;
  background: #64748b;
}
.rs-spd__ticks option:empty::before {
  top: 3px;
  height: 5px;
}

Step slider with named stops

  • step
  • aria-valuetext
  • data attribute colors

Basil chicken stir-fry $14.50

Medium

Drag the thumb or focus it and press the arrow keys.

Four stops, each with a name. The script mirrors the value into a data attribute, and CSS uses it to recolor the fill and highlight the matching label. aria-valuetext announces Hot rather than 2, and the labels are hidden from assistive technology because the slider already says the same thing.

View code
.rs-sp {
  --rs-sp-c: #a16207;
  box-sizing: border-box;
  width: min(100%, 380px);
  padding: 22px 24px;
  border-radius: 18px;
  background: #fffaf3;
  color: #2b1d14;
  box-shadow: 0 0 0 1px #f0e2cf, 0 12px 28px rgba(43, 29, 20, .08);
  font: inherit;
}
.rs-sp[data-level="1"] {
  --rs-sp-c: #c2410c;
}
.rs-sp[data-level="2"] {
  --rs-sp-c: #b91c1c;
}
.rs-sp[data-level="3"] {
  --rs-sp-c: #7f1d1d;
}
.rs-sp__dish {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid #ecdcc6;
  font-family: "Fraunces", Georgia, serif;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: 0;
  color: #2b1d14;
}
.rs-sp__dish span {
  font-family: inherit;
  font-weight: 400;
  color: #6b5444;
}
.rs-sp__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}
.rs-sp__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-sp__out {
  font-size: 18px;
  font-weight: 700;
  color: var(--rs-sp-c);
}
.rs-sp__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-sp__range::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(to right, var(--rs-sp-c) calc(12px + var(--rs-sp-v) * (100% - 24px)), #a8998a 0);
}
.rs-sp__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 24px;
  height: 24px;
  margin-top: -7px;
  border: 3px solid var(--rs-sp-c);
  border-radius: 50%;
  background: #fff;
}
.rs-sp__range::-moz-range-track {
  height: 10px;
  border-radius: 5px;
  background: #a8998a;
}
.rs-sp__range::-moz-range-progress {
  height: 10px;
  border-radius: 5px;
  background: var(--rs-sp-c);
}
.rs-sp__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 24px;
  height: 24px;
  border: 3px solid var(--rs-sp-c);
  border-radius: 50%;
  background: #fff;
}
.rs-sp__range:focus-visible {
  outline: 3px solid var(--rs-sp-c);
  outline-offset: 2px;
  border-radius: 8px;
}
.rs-sp__stops {
  display: flex;
  justify-content: space-between;
  padding: 4px 12px 0;
}
.rs-sp__stops span {
  display: flex;
  justify-content: center;
  width: 0;
  font-size: 13px;
  color: #6b5444;
  white-space: nowrap;
}
.rs-sp__stops span:first-child {
  justify-content: flex-start;
  margin-left: -12px;
}
.rs-sp__stops span:last-child {
  justify-content: flex-end;
  margin-right: -12px;
}
.rs-sp[data-level="0"] .rs-sp__stops span:nth-child(1),
.rs-sp[data-level="1"] .rs-sp__stops span:nth-child(2),
.rs-sp[data-level="2"] .rs-sp__stops span:nth-child(3),
.rs-sp[data-level="3"] .rs-sp__stops span:nth-child(4) {
  font-weight: 700;
  color: var(--rs-sp-c);
}

Rating slider with drawn faces

  • inline SVG
  • setAttribute("d")
  • aria-valuetext
Good

Drag the thumb or focus it and press the arrow keys.

A five-point rating as a slider. The script swaps the mouth path of an inline SVG face and sets a data attribute that recolors the face and fill, and the visible word plus aria-valuetext (4 of 5, Good) make the number meaningful. The face is decorative, so it is hidden from screen readers.

View code
.rs-rt {
  --rs-rt-skin: #b9e4a1;
  --rs-rt-c: #2f6b1f;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 20px;
  width: min(100%, 420px);
  padding: 22px;
  border-radius: 20px;
  background: #fff;
  color: #1f2937;
  box-shadow: 0 12px 30px rgba(31, 41, 55, .09);
  font: inherit;
}
.rs-rt[data-level="1"] {
  --rs-rt-skin: #f8b4b4;
  --rs-rt-c: #9b1c1c;
}
.rs-rt[data-level="2"] {
  --rs-rt-skin: #fbcf9b;
  --rs-rt-c: #9a4a07;
}
.rs-rt[data-level="3"] {
  --rs-rt-skin: #f6e27a;
  --rs-rt-c: #735c00;
}
.rs-rt[data-level="5"] {
  --rs-rt-skin: #8fd8b4;
  --rs-rt-c: #0f6b47;
}
.rs-rt__face {
  flex: none;
  display: block;
}
.rs-rt__skin {
  fill: var(--rs-rt-skin);
  stroke: #1f2937;
  stroke-width: 2;
  transition: fill .2s ease;
}
.rs-rt__eye {
  fill: #1f2937;
}
.rs-rt__mouth {
  fill: none;
  stroke: #1f2937;
  stroke-width: 2.4;
  stroke-linecap: round;
}
.rs-rt__body {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 4px 10px;
  flex: 1;
  min-width: 0;
}
.rs-rt__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-rt__out {
  font-size: 15px;
  font-weight: 700;
  color: var(--rs-rt-c);
}
.rs-rt__range {
  -webkit-appearance: none;
  appearance: none;
  grid-column: 1 / -1;
  display: block;
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-rt__range::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(to right, #1f2937 calc(11px + var(--rs-rt-v) * (100% - 22px)), #9aa3af 0);
}
.rs-rt__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  margin-top: -7px;
  border: 3px solid #1f2937;
  border-radius: 50%;
  background: var(--rs-rt-skin);
}
.rs-rt__range::-moz-range-track {
  height: 8px;
  border-radius: 4px;
  background: #9aa3af;
}
.rs-rt__range::-moz-range-progress {
  height: 8px;
  border-radius: 4px;
  background: #1f2937;
}
.rs-rt__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  border: 3px solid #1f2937;
  border-radius: 50%;
  background: var(--rs-rt-skin);
}
.rs-rt__range:focus-visible {
  outline: 3px solid #1f2937;
  outline-offset: 2px;
  border-radius: 6px;
}
@media (max-width: 400px) {
  .rs-rt {
    flex-direction: column;
    align-items: stretch;
    text-align: left;
  }
  .rs-rt__face {
    align-self: center;
  }
}

Vertical equalizer sliders

  • writing-mode: vertical-lr
  • direction: rtl
  • min() / max() stops

Equalizer

+4
+2
-3
0
+6

Drag the thumb or focus it and press the arrow keys.

Each band is a normal range input turned upright with writing-mode: vertical-lr, and direction: rtl puts the minimum at the bottom. The fill grows from the center line in either direction, using min() and max() as the gradient stops. Vertical range inputs need Chrome 124, Firefox 120 or Safari 16.5, and older browsers draw them horizontally.

View code
.rs-eq {
  box-sizing: border-box;
  width: min(100%, 400px);
  padding: 20px 18px 16px;
  border-radius: 18px;
  background: #1b1d23;
  color: #eef0f4;
  box-shadow: 0 0 0 1px #2c2f38;
  font: inherit;
}
.rs-eq__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.rs-eq__title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: #eef0f4;
}
.rs-eq__reset {
  min-height: 32px;
  padding: 4px 14px;
  border: 1px solid #5d6372;
  border-radius: 999px;
  background: transparent;
  color: #eef0f4;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.rs-eq__reset:hover {
  border-color: #f5b942;
}
.rs-eq__reset:focus-visible {
  outline: 3px solid #f5b942;
  outline-offset: 2px;
}
.rs-eq__bands {
  display: flex;
  justify-content: space-between;
  gap: 4px;
}
.rs-eq__band {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.rs-eq__out {
  font-size: 13px;
  font-weight: 700;
  color: #f5b942;
  font-variant-numeric: tabular-nums;
}
.rs-eq__label {
  font-size: 12px;
  color: #b4b9c6;
  white-space: nowrap;
}
.rs-eq__range {
  --rs-eq-v: .5;
  -webkit-appearance: none;
  appearance: none;
  writing-mode: vertical-lr;
  direction: rtl;
  display: block;
  width: 32px;
  height: 160px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-eq__range::-webkit-slider-runnable-track {
  width: 6px;
  border-radius: 3px;
  background: linear-gradient(to top, #6b7180 min(50%, calc(9px + var(--rs-eq-v) * (100% - 18px))), #f5b942 0 max(50%, calc(9px + var(--rs-eq-v) * (100% - 18px))), #6b7180 0);
}
.rs-eq__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 28px;
  height: 18px;
  margin-left: -11px;
  border: 2px solid #1b1d23;
  border-radius: 6px;
  background: #eef0f4;
  box-shadow: inset 0 0 0 4px #eef0f4, inset 0 0 0 5px #8c92a1;
}
.rs-eq__range::-moz-range-track {
  width: 6px;
  border-radius: 3px;
  background: linear-gradient(to top, #6b7180 min(50%, calc(9px + var(--rs-eq-v) * (100% - 18px))), #f5b942 0 max(50%, calc(9px + var(--rs-eq-v) * (100% - 18px))), #6b7180 0);
}
.rs-eq__range::-moz-range-progress {
  background: transparent;
}
.rs-eq__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 28px;
  height: 18px;
  border: 2px solid #1b1d23;
  border-radius: 6px;
  background: #eef0f4;
  box-shadow: inset 0 0 0 4px #eef0f4, inset 0 0 0 5px #8c92a1;
}
.rs-eq__range:focus-visible {
  outline: 3px solid #f5b942;
  outline-offset: 2px;
  border-radius: 6px;
}

Range sliders in real interfaces

Sliders doing a job inside a component: a two-handle price filter, a volume control with mute, a smart-home card and a slider that drives a live text preview.

Two-handle price range

  • two stacked inputs
  • pointer-events on thumbs
  • clamping script

Price

$400 to $1,400

38 bikes match

Two full-width range inputs sit on top of each other with pointer-events: none, and only their thumbs take pointer events back. Each slider has its own label, the script stops the handles from crossing with a 100 dollar gap, and two number fields give a way to set the range without dragging.

View code
.rs-pr {
  box-sizing: border-box;
  width: min(100%, 400px);
  padding: 22px 24px;
  border-radius: 14px;
  background: #fff;
  color: #1a1c20;
  box-shadow: 0 0 0 1px #e4e6ea, 0 12px 28px rgba(26, 28, 32, .07);
  font: inherit;
}
.rs-pr__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
  margin-bottom: 8px;
}
.rs-pr__title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
}
.rs-pr__out {
  font-size: 17px;
  font-weight: 700;
  color: #b93a16;
  font-variant-numeric: tabular-nums;
}
.rs-pr__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.rs-pr__slider {
  position: relative;
  height: 36px;
}
.rs-pr__slider::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 0;
  left: 0;
  height: 6px;
  margin-top: -3px;
  border-radius: 3px;
  background: linear-gradient(to right, #8b93a1 calc(11px + var(--rs-pr-a) * (100% - 22px)), #e2552d 0 calc(11px + var(--rs-pr-b) * (100% - 22px)), #8b93a1 0);
}
.rs-pr__range {
  -webkit-appearance: none;
  appearance: none;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 100%;
  height: 36px;
  margin: 0;
  padding: 0;
  background: transparent;
  pointer-events: none;
}
.rs-pr__range--max,
.rs-pr__slider[data-front="max"] .rs-pr__range--max {
  z-index: 2;
}
.rs-pr__slider[data-front="min"] .rs-pr__range--min {
  z-index: 3;
}
.rs-pr__range::-webkit-slider-runnable-track {
  height: 36px;
  background: transparent;
}
.rs-pr__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  margin-top: 7px;
  border: 3px solid #e2552d;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(26, 28, 32, .25);
  cursor: pointer;
  pointer-events: auto;
}
.rs-pr__range::-moz-range-track {
  background: transparent;
}
.rs-pr__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  border: 3px solid #e2552d;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(26, 28, 32, .25);
  cursor: pointer;
  pointer-events: auto;
}
.rs-pr__range:focus-visible {
  outline: none;
}
.rs-pr__range:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px #fff, 0 0 0 6px #1a1c20;
}
.rs-pr__range:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px #fff, 0 0 0 6px #1a1c20;
}
.rs-pr__fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 12px;
}
.rs-pr__field {
  display: grid;
  gap: 4px;
  min-width: 0;
}
.rs-pr__flabel {
  font-size: 13px;
  font-weight: 600;
  color: #4b5160;
}
.rs-pr__num {
  box-sizing: border-box;
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: 8px 10px;
  border: 1px solid #8b93a1;
  border-radius: 8px;
  background: #fff;
  color: #1a1c20;
  font: inherit;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.rs-pr__num:focus-visible {
  outline: 3px solid #e2552d;
  outline-offset: 1px;
}
.rs-pr__count {
  margin: 14px 0 0;
  font-size: 14px;
  color: #4b5160;
}
.rs-pr__n {
  font-weight: 700;
  color: #1a1c20;
}

Volume slider with a mute toggle

  • aria-pressed
  • data attribute icon states
  • aria-valuetext

Harbor Lights

The Quiet Fleet

60%

The speaker icon is a real toggle button with aria-pressed, and its waves follow the volume level through a data attribute. Muting keeps the slider value, greys the fill and changes aria-valuetext to Muted, 60 percent, and dragging the slider unmutes, which is how most media players behave.

View code
.rs-vol {
  --rs-vol-c: #1e3a5f;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 14px 16px;
  align-items: center;
  width: min(100%, 380px);
  padding: 18px;
  border-radius: 20px;
  background: #f3efe6;
  color: #1e3a5f;
  box-shadow: 0 0 0 1px #e3dccd;
  font: inherit;
}
.rs-vol[data-muted="true"] {
  --rs-vol-c: #6a7380;
}
.rs-vol__art svg {
  display: block;
  border-radius: 12px;
}
.rs-vol__meta {
  min-width: 0;
}
.rs-vol__track {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0;
  color: #1e3a5f;
}
.rs-vol__artist {
  margin: 2px 0 0;
  font-size: 14px;
  color: #4d5b6c;
}
.rs-vol__row {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
}
.rs-vol__mute {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #fff;
  color: #1e3a5f;
  cursor: pointer;
}
.rs-vol__mute:hover {
  background: #e6edf5;
}
.rs-vol__mute:focus-visible {
  outline: 3px solid #1e3a5f;
  outline-offset: 2px;
}
.rs-vol__mute svg {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.rs-vol__spk {
  fill: currentColor;
}
.rs-vol__x {
  display: none;
}
.rs-vol[data-level="low"] .rs-vol__w2,
.rs-vol[data-level="off"] .rs-vol__w1,
.rs-vol[data-level="off"] .rs-vol__w2,
.rs-vol[data-muted="true"] .rs-vol__w1,
.rs-vol[data-muted="true"] .rs-vol__w2 {
  display: none;
}
.rs-vol[data-muted="true"] .rs-vol__x,
.rs-vol[data-level="off"] .rs-vol__x {
  display: inline;
}
.rs-vol__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.rs-vol__range {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 0;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-vol__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background: linear-gradient(to right, var(--rs-vol-c) calc(9px + var(--rs-vol-v) * (100% - 18px)), #a59d8c 0);
}
.rs-vol__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 18px;
  height: 18px;
  margin-top: -6px;
  border: 0;
  border-radius: 50%;
  background: var(--rs-vol-c);
  box-shadow: 0 0 0 3px #f3efe6;
}
.rs-vol__range::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background: #a59d8c;
}
.rs-vol__range::-moz-range-progress {
  height: 6px;
  border-radius: 3px;
  background: var(--rs-vol-c);
}
.rs-vol__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: 50%;
  background: var(--rs-vol-c);
  box-shadow: 0 0 0 3px #f3efe6;
}
.rs-vol__range:focus-visible {
  outline: 3px solid #1e3a5f;
  outline-offset: 2px;
  border-radius: 6px;
}
.rs-vol__out {
  flex: none;
  width: 52px;
  font-size: 14px;
  font-weight: 700;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

Brightness and temperature card

  • step="0.5"
  • aria-valuetext
  • custom property opacity

Living room

70%
21.5 °C

Room is 20 °C, heating to 21.5 °C

A smart-home card with two sliders. Brightness feeds the opacity of the lamp glow, and the thermostat steps in half degrees, announces 21.5 degrees Celsius through aria-valuetext and compares the target with the room to say whether it is heating or cooling.

View code
.rs-sh {
  box-sizing: border-box;
  display: grid;
  gap: 18px;
  width: min(100%, 360px);
  padding: 22px;
  border-radius: 26px;
  background: #fff;
  color: #222326;
  box-shadow: 0 16px 40px rgba(34, 35, 38, .1);
  font: inherit;
}
.rs-sh__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.rs-sh__room {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  color: #222326;
}
.rs-sh__lamp {
  display: block;
  flex: none;
}
.rs-sh__glow {
  fill: #f6b93b;
  opacity: calc(var(--rs-sh-b) * .45);
}
.rs-sh__bulb {
  fill: #fff;
  stroke: #222326;
  stroke-width: 2;
}
.rs-sh__base {
  fill: none;
  stroke: #222326;
  stroke-width: 2;
  stroke-linecap: round;
}
.rs-sh__ctl {
  padding: 14px 16px 12px;
  border-radius: 16px;
  background: #f4f4f1;
}
.rs-sh__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.rs-sh__label {
  font-size: 14px;
  font-weight: 600;
}
.rs-sh__out {
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.rs-sh__range {
  --rs-sh-c: #b7791f;
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 36px;
  margin: 2px 0 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-sh__range--b {
  --rs-sh-v: var(--rs-sh-b);
}
.rs-sh__range--t {
  --rs-sh-c: #c2410c;
  --rs-sh-v: var(--rs-sh-t);
}
.rs-sh__range::-webkit-slider-runnable-track {
  height: 24px;
  border-radius: 12px;
  background: linear-gradient(to right, var(--rs-sh-c) calc(12px + var(--rs-sh-v) * (100% - 24px)), #c9c8c1 0);
}
.rs-sh__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 24px;
  height: 24px;
  border: 4px solid var(--rs-sh-c);
  border-radius: 50%;
  background: #fff;
}
.rs-sh__range::-moz-range-track {
  height: 24px;
  border-radius: 12px;
  background: #c9c8c1;
}
.rs-sh__range::-moz-range-progress {
  height: 24px;
  border-radius: 12px;
  background: var(--rs-sh-c);
}
.rs-sh__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 24px;
  height: 24px;
  border: 4px solid var(--rs-sh-c);
  border-radius: 50%;
  background: #fff;
}
.rs-sh__range:focus-visible {
  outline: 3px solid #222326;
  outline-offset: 2px;
  border-radius: 14px;
}
.rs-sh__status {
  margin: 6px 0 0;
  font-size: 13px;
  color: #55565c;
}
.rs-sh__mode {
  color: #222326;
}

Slider driving a live text preview

  • custom property in px
  • aria-valuetext
  • line-height in em
18px

The harbour at first light

Fishing boats leave before the ferries, and by seven the quay smells of diesel and fresh bread.

The slider writes its value to --rs-fs-size and the preview reads it, so the text resizes as you drag. The heading scales from the same value. The raw number becomes 18 pixels in aria-valuetext, and the small and large letters at the ends are decorative hints, hidden from screen readers.

View code
.rs-fs {
  --rs-fs-v: .3;
  box-sizing: border-box;
  width: min(100%, 460px);
  border-radius: 14px;
  background: #fff;
  color: #1f1a17;
  box-shadow: 0 0 0 1px #e7e2dc, 0 12px 30px rgba(31, 26, 23, .08);
  overflow: hidden;
  font: inherit;
}
.rs-fs__bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px 0;
}
.rs-fs__label {
  font-size: 14px;
  font-weight: 600;
}
.rs-fs__out {
  font-size: 14px;
  font-weight: 700;
  color: #7c2d12;
  font-family: ui-monospace, Consolas, monospace;
}
.rs-fs__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 4px 20px 14px;
  border-bottom: 1px solid #ece7e1;
}
.rs-fs__a {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 600;
  color: #5c534c;
  line-height: 1;
}
.rs-fs__a--s {
  font-size: 13px;
}
.rs-fs__a--l {
  font-size: 24px;
}
.rs-fs__range {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 0;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-fs__range::-webkit-slider-runnable-track {
  height: 2px;
  background: #857b73;
}
.rs-fs__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 14px;
  height: 26px;
  margin-top: -12px;
  border: 2px solid #1f1a17;
  border-radius: 4px;
  background: #fff;
}
.rs-fs__range::-moz-range-track {
  height: 2px;
  background: #857b73;
}
.rs-fs__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 14px;
  height: 26px;
  border: 2px solid #1f1a17;
  border-radius: 4px;
  background: #fff;
}
.rs-fs__range:focus-visible {
  outline: 3px solid #7c2d12;
  outline-offset: 2px;
  border-radius: 4px;
}
.rs-fs__preview {
  padding: 18px 20px 22px;
  background: #fbf9f6;
}
.rs-fs__h {
  margin: 0 0 .4em;
  font-family: "Fraunces", Georgia, serif;
  font-size: calc(var(--rs-fs-size) * 1.45);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: 0;
  color: #1f1a17;
  overflow-wrap: anywhere;
}
.rs-fs__p {
  margin: 0;
  font-size: var(--rs-fs-size);
  line-height: 1.55;
  color: #3b342f;
  overflow-wrap: anywhere;
}

Color, size, theme and state

A hue picker, a large touch slider, a dark media scrubber and a disabled state that explains itself. Each one keeps a visible label and a visible value.

Hue slider with a live swatch

  • hsl() spectrum track
  • thumb color from var
  • contrast check in JS
212°
hsl(212 72% 42%) #1e69b9

The one demo where the track gradient is functional: it is the hue wheel unrolled, with each stop placed on the thumb travel so the color under the thumb center matches the value. The thumb fills with the chosen hue, and the script picks black or white button text by luminance so the preview stays readable.

View code
.rs-hue {
  --rs-hue-ink: #fff;
  box-sizing: border-box;
  width: min(100%, 400px);
  padding: 22px 24px;
  border-radius: 16px;
  background: #fff;
  color: #18181b;
  box-shadow: 0 0 0 1px #e4e4e7, 0 12px 30px rgba(24, 24, 27, .08);
  font: inherit;
}
.rs-hue__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.rs-hue__label {
  font-size: 15px;
  font-weight: 600;
}
.rs-hue__out {
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.rs-hue__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-hue__range::-webkit-slider-runnable-track {
  height: 14px;
  border-radius: 7px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25);
  background: linear-gradient(to right, hsl(0 85% 50%) 13px, hsl(60 85% 50%) calc(13px + (100% - 26px) / 6), hsl(120 85% 45%) calc(13px + (100% - 26px) / 3), hsl(180 85% 45%) calc(13px + (100% - 26px) / 2), hsl(240 85% 55%) calc(13px + (100% - 26px) * 2 / 3), hsl(300 85% 50%) calc(13px + (100% - 26px) * 5 / 6), hsl(360 85% 50%) calc(100% - 13px));
}
.rs-hue__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 26px;
  height: 26px;
  margin-top: -6px;
  border: 4px solid #fff;
  border-radius: 50%;
  background: hsl(var(--rs-hue-h) 72% 42%);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .3);
}
.rs-hue__range::-moz-range-track {
  height: 14px;
  border-radius: 7px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25);
  background: linear-gradient(to right, hsl(0 85% 50%) 13px, hsl(60 85% 50%) calc(13px + (100% - 26px) / 6), hsl(120 85% 45%) calc(13px + (100% - 26px) / 3), hsl(180 85% 45%) calc(13px + (100% - 26px) / 2), hsl(240 85% 55%) calc(13px + (100% - 26px) * 2 / 3), hsl(300 85% 50%) calc(13px + (100% - 26px) * 5 / 6), hsl(360 85% 50%) calc(100% - 13px));
}
.rs-hue__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 26px;
  height: 26px;
  border: 4px solid #fff;
  border-radius: 50%;
  background: hsl(var(--rs-hue-h) 72% 42%);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .3);
}
.rs-hue__range:focus-visible {
  outline: 3px solid #18181b;
  outline-offset: 2px;
  border-radius: 10px;
}
.rs-hue__preview {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid #ececef;
}
.rs-hue__swatch {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: hsl(var(--rs-hue-h) 72% 42%);
}
.rs-hue__codes {
  display: grid;
  flex: 1;
  min-width: 120px;
  font-size: 13px;
}
.rs-hue__codes code {
  padding: 0;
  background: none;
  color: #3f3f46;
  font-family: ui-monospace, Consolas, monospace;
}
.rs-hue__btn {
  padding: 10px 16px;
  border-radius: 8px;
  background: hsl(var(--rs-hue-h) 72% 42%);
  color: var(--rs-hue-ink);
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
}

Large touch slider for a tip screen

  • 44px thumb
  • tall hit area
  • aria-valuetext

Order total $40.00

18% $7.20

Built for a countertop tablet: a 44px thumb, a 20px track and a 56px tall input, so the whole control is an easy target. The value is printed large with the dollar amount beside it, and aria-valuetext reads 18 percent tip, $7.20 instead of a bare 18.

View code
.rs-tip {
  box-sizing: border-box;
  width: min(100%, 420px);
  padding: 24px;
  border-radius: 24px;
  background: #fdf6e9;
  color: #14302b;
  box-shadow: 0 0 0 1px #efe2c8;
  font: inherit;
  text-align: center;
}
.rs-tip__order {
  margin: 0 0 16px;
  font-size: 14px;
  color: #45524f;
}
.rs-tip__order strong {
  color: #14302b;
}
.rs-tip__label {
  display: block;
  font-size: 18px;
  font-weight: 700;
}
.rs-tip__value {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 12px;
  margin: 4px 0 10px;
}
.rs-tip__out {
  font-size: 48px;
  font-weight: 700;
  line-height: 1;
  color: #0f766e;
  font-variant-numeric: tabular-nums;
}
.rs-tip__amt {
  font-size: 20px;
  font-weight: 600;
  color: #14302b;
  font-variant-numeric: tabular-nums;
}
.rs-tip__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 56px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  touch-action: pan-y;
}
.rs-tip__range::-webkit-slider-runnable-track {
  height: 20px;
  border-radius: 10px;
  background: linear-gradient(to right, #0f766e calc(22px + var(--rs-tip-v) * (100% - 44px)), #a99e88 0);
}
.rs-tip__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 44px;
  height: 44px;
  margin-top: -12px;
  border: 5px solid #0f766e;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 3px 10px rgba(20, 48, 43, .25);
}
.rs-tip__range::-moz-range-track {
  height: 20px;
  border-radius: 10px;
  background: #a99e88;
}
.rs-tip__range::-moz-range-progress {
  height: 20px;
  border-radius: 10px;
  background: #0f766e;
}
.rs-tip__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 44px;
  height: 44px;
  border: 5px solid #0f766e;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 3px 10px rgba(20, 48, 43, .25);
}
.rs-tip__range:focus-visible {
  outline: 3px solid #14302b;
  outline-offset: 2px;
  border-radius: 28px;
}
.rs-tip__scale {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 13px;
  color: #45524f;
}

Dark podcast scrubber

  • dark theme tokens
  • aria-valuetext time
  • skip buttons

Field Notes FM

Ep. 48: Why every map lies a little

A playback scrubber for a dark player. The value is seconds, which means nothing read aloud, so aria-valuetext says 12 minutes 34 seconds of 41 minutes. The thumb grows on hover and focus, the unfilled track keeps 3:1 contrast against the card, and the skip buttons move the same input.

View code
.rs-pod {
  box-sizing: border-box;
  width: min(100%, 420px);
  padding: 22px 22px 18px;
  border-radius: 18px;
  background: #16181d;
  color: #f2f3f5;
  box-shadow: 0 0 0 1px #2a2d35;
  font: inherit;
}
.rs-pod__show {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: #ff8f73;
}
.rs-pod__ep {
  margin: 4px 0 16px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0;
  color: #f2f3f5;
}
.rs-pod__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.rs-pod__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 28px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-pod__range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(to right, #ff8f73 calc(7px + var(--rs-pod-v) * (100% - 14px)), #6b7180 0);
}
.rs-pod__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border: 0;
  border-radius: 50%;
  background: #fff;
}
.rs-pod__range::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: #6b7180;
}
.rs-pod__range::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: #ff8f73;
}
.rs-pod__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 14px;
  height: 14px;
  border: 0;
  border-radius: 50%;
  background: #fff;
}
.rs-pod__range:hover::-webkit-slider-thumb,
.rs-pod__range:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 5px rgba(255, 143, 115, .35);
}
.rs-pod__range:hover::-moz-range-thumb,
.rs-pod__range:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 5px rgba(255, 143, 115, .35);
}
.rs-pod__range:focus-visible {
  outline: 2px solid #ff8f73;
  outline-offset: 2px;
  border-radius: 6px;
}
.rs-pod__times {
  display: flex;
  justify-content: space-between;
  margin-top: 2px;
  font-size: 13px;
  color: #b9bdc7;
  font-variant-numeric: tabular-nums;
}
.rs-pod__btns {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 12px;
}
.rs-pod__skip {
  min-width: 64px;
  min-height: 44px;
  padding: 6px 14px;
  border: 1px solid #4a4f5c;
  border-radius: 999px;
  background: #23262e;
  color: #f2f3f5;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.rs-pod__skip:hover {
  background: #2d313b;
}
.rs-pod__skip:focus-visible {
  outline: 2px solid #ff8f73;
  outline-offset: 2px;
}

Disabled slider that explains why

  • :disabled
  • pseudo-elements on :disabled
  • aria-describedby

Cloud backup

200 GB

Automatic sizing is on. Turn it off to set your own limit.

While automatic sizing is on, the slider is truly disabled, so it leaves the tab order and ignores drags. The :disabled state greys the thumb and fill through the same pseudo-element rules, and a hint tied with aria-describedby says how to unlock it instead of leaving a dead control.

View code
.rs-bk {
  --rs-bk-c: #3949ab;
  box-sizing: border-box;
  width: min(100%, 380px);
  padding: 22px 24px;
  border-radius: 14px;
  background: #fff;
  color: #1b1f2a;
  box-shadow: 0 0 0 1px #e3e6ed, 0 12px 28px rgba(27, 31, 42, .07);
  font: inherit;
}
.rs-bk__title {
  margin: 0 0 12px;
  font-size: 17px;
  font-weight: 700;
  color: #1b1f2a;
}
.rs-bk__check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin-bottom: 10px;
  font-size: 14px;
}
.rs-bk__check label {
  cursor: pointer;
}
.rs-bk__auto {
  flex: none;
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #3949ab;
  cursor: pointer;
}
.rs-bk__auto:focus-visible {
  outline: 3px solid #3949ab;
  outline-offset: 2px;
}
.rs-bk__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.rs-bk__label {
  font-size: 14px;
  font-weight: 600;
}
.rs-bk__out {
  font-size: 18px;
  font-weight: 700;
  color: var(--rs-bk-c);
  font-variant-numeric: tabular-nums;
}
.rs-bk:has(.rs-bk__range:disabled) {
  --rs-bk-c: #5f6673;
}
.rs-bk__range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 32px;
  margin: 2px 0 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.rs-bk__range:disabled {
  cursor: not-allowed;
}
.rs-bk__range::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(to right, #3949ab calc(11px + var(--rs-bk-v) * (100% - 22px)), #8e95a5 0);
}
.rs-bk__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  margin-top: -7px;
  border: 0;
  border-radius: 50%;
  background: #3949ab;
  box-shadow: inset 0 0 0 7px #3949ab, inset 0 0 0 11px #fff;
}
.rs-bk__range::-moz-range-track {
  height: 8px;
  border-radius: 4px;
  background: #8e95a5;
}
.rs-bk__range::-moz-range-progress {
  height: 8px;
  border-radius: 4px;
  background: #3949ab;
}
.rs-bk__range::-moz-range-thumb {
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 50%;
  background: #3949ab;
  box-shadow: inset 0 0 0 7px #3949ab, inset 0 0 0 11px #fff;
}
.rs-bk__range:disabled::-webkit-slider-runnable-track {
  background: linear-gradient(to right, #9aa0ac calc(11px + var(--rs-bk-v) * (100% - 22px)), #d5d9e0 0);
}
.rs-bk__range:disabled::-webkit-slider-thumb {
  background: #b5bac4;
  box-shadow: none;
}
.rs-bk__range:disabled::-moz-range-track {
  background: #d5d9e0;
}
.rs-bk__range:disabled::-moz-range-progress {
  background: #9aa0ac;
}
.rs-bk__range:disabled::-moz-range-thumb {
  background: #b5bac4;
  box-shadow: none;
}
.rs-bk__range:focus-visible {
  outline: 3px solid #3949ab;
  outline-offset: 2px;
  border-radius: 8px;
}
.rs-bk__hint {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: #545b68;
}

How does the native range input work?

Dragging, keyboard input and value tracking all belong to the browser. The user slides a thumb along a track, and CSS only restyles what is already there.

Behavior comes from a handful of ordinary HTML attributes. Min sets the lowest allowed value and defaults to 0, while max sets the highest and defaults to 100. Step controls the interval between values (1 by default, or the keyword any for no snapping). Value is the starting position, and it lands halfway between min and max unless you set it.

Those defaults come from the MDN Web Docs reference for the range input, revised in September 2026.

Set max equal to min, or below it, and the slider stops responding to the user. When a scripted value ignores the step, browsers round it to the nearest valid step and prefer the higher one on a tie.

Support is not a concern. MDN marks the range input as Baseline widely available, working across browsers since March 2017.

The control needs no JavaScript to work. Script only enters when the page has to display the current value, and the input event and the output element handle that.

The element also carries an implicit ARIA role of slider, so screen readers announce it without extra markup.

The pieces open to styling are the thumb, which is the draggable knob, and the track it slides in. There’s also the fill, the stretch of track behind the thumb, though browsers treat that one very unevenly. All of them are reached through vendor-specific pseudo-elements.

How to style the thumb and track

Strip the native look with appearance: none, then style the track and thumb through engine-specific pseudo-elements. Chrome, Safari and Edge use ::-webkit-slider-runnable-track and ::-webkit-slider-thumb. Firefox uses ::-moz-range-track and ::-moz-range-thumb.

  1. Set appearance: none on the input.
  2. Give the track an explicit height, a background and a border radius.
  3. Size the thumb explicitly, then offset it so it centers on the track in Chrome and Safari.
  4. Add :focus-visible and :active states to the thumb.

Reset the default appearance

Nothing written for the thumb or track applies until the native look is gone.

Set appearance: none on the input and on the thumb. Ana Tudor found in 2017 that setting it on only one of the two changes nothing in Chrome (CSS-Tricks).

The reset has a side effect. In her Chrome tests the thumb dropped from 10 by 21 pixels to 129 by 0, and the track and input collapsed with it, so the slider vanished until the thumb got explicit dimensions.

Style the track

Set the height explicitly. In Ana Tudor’s tests, Firefox defaulted to 0.2em, while Chrome derived the height from the thumb, so leaving it alone gives you two different sliders. The background is the unfilled color and can be a solid or a gradient. A border radius at half the height gives a pill-shaped track.

Size the width on the input, not the track. Chrome ignored a width set on the track in the same 2017 tests and stretched it to match the input.

Style the thumb

After the reset, Chrome aligns the top edge of the thumb with the top of the track, not its middle. Firefox centers it automatically.

The fix is a negative margin-top on the ::-webkit-slider-thumb rule only: track height minus thumb height, divided by 2. A 4px track with a 16px thumb takes margin-top: -6px.

If the track has a border, subtract twice its width from the track height before halving.

Round the thumb with border-radius: 50%. A soft shadow adds depth, and a box shadow generator speeds up tuning the values.

Leave margin-top out of the Firefox rule. Firefox already centers the thumb, so the same offset pushes it off the track.

Why does a range slider look different across browsers?

Each engine assembles the slider from different internal parts and gives them different names. Blink and WebKit nest the thumb inside the track, Gecko places track, fill and thumb side by side, and among current engines only Gecko offers a native fill.

PartChrome, Safari, EdgeFirefoxIE and old Edge
Thumb::-webkit-slider-thumb::-moz-range-thumb::-ms-thumb
Track::-webkit-slider-runnable-track::-moz-range-track::-ms-track
FillNone::-moz-range-progress::-ms-fill-lower

The nesting changes behavior. In Ana Tudor’s 2017 CSS-Tricks tests, opacity, filter or transform on the track also hit the thumb in Chrome, but not in Firefox, where the two are siblings.

A selector list containing one pseudo-element the browser does not recognize is dropped whole. Write one rule per engine and never group the vendor selectors, a point the postcss-input-style documentation makes by citing the Selectors Level 3 specification.

Grouped selectors are a common source of cross-browser compatibility bugs in slider CSS. If a custom thumb shows in Chrome but not in Firefox, first confirm that a ::-moz-range-thumb rule exists and that it isn’t sharing a list with the WebKit selector.

Legacy ::-ms- rules only matter for Internet Explorer and the old EdgeHTML-based Edge. Microsoft retired the IE11 desktop app on June 15, 2022, per its retirement notice, so new projects can drop them.

Default dimensions differ too, so set an explicit width on the input rather than trusting the browser.

Test in Chrome, Safari and Firefox, and drag the thumb to both ends in each. Thumb travel is not identical. Chrome limits it to the track’s content box and Firefox to the input’s, according to the same CSS-Tricks analysis.

How far does accent-color go on a range slider?

The accent-color property recolors the slider with one declaration, and it stops at color. Track height, thumb size, thumb shape and tick marks stay with the browser.

The property is inherited, so accent-color on :root tints every supported control on the page. web.dev (Chrome team, 2021) lists checkbox, radio, range and progress among them.

The browser picks a contrast color to pair with your accent, and it does not change your accent in a dark theme. Set color-scheme: light dark so both themes get tinted, then check the accent’s color contrast against the track in each.

Contrast algorithms differ by engine. web.dev’s 2021 comparison showed Chrome 94 and Firefox 92 Nightly pairing the same accent with different contrast colors.

The appeal is obvious. It takes one declaration and no pseudo-elements, native keyboard behavior and focus handling stay intact, and the browser deals with contrast against the track.

The catch is that it only does color. Height, size and shape still need pseudo-elements. The browser, not you, chooses the companion color, you get no separate control over the unfilled track, and MDN flags the property as limited availability, not Baseline.

Sources disagree on support. Some tutorials call it well supported across browsers, while MDN’s page (revised September 2026) labels it limited availability.

MDN also notes that some browsers return a fixed value for accent-color: auto to reduce fingerprinting risk. Check Can I use before relying on it. An unsupported browser shows the default colors, so the page still works.

If the design only needs a brand color, stop here. If it needs a thicker track, a custom thumb or a different unfilled color, move to pseudo-element styling.

How to fill the track and show the current value

Store the thumb position in a CSS custom property, then use it as the hard stop of a linear-gradient on the track background. A few lines of script update the property on every input event, and an output element prints the number.

Fill the track with a custom property

The gradient needs one number, the thumb position as a fraction between 0 and 1, calculated as (value minus min) divided by (max minus min). Write it to a property such as –val from the input event listener, the approach CSS-Tricks documents for range inputs.

On the track background, put two gradient stops at the same position, with the accent color on the left and the unfilled color on the right. The stop position is a calc() that multiplies –val by the usable width. Set –val in the stylesheet to match the input’s starting value as well, or the fill starts empty until the script runs.

That stop position is where hand-built fills go wrong. The thumb stays inside the track, so at the minimum its center sits half a thumb width from the left edge, and a stop at 0% leaves that strip unfilled.

Take a 16px thumb on a 200px track. The center travels from 8px to 192px, so the stop is calc(8px + var(–val) * (100% – 16px)).

Firefox can use the same gradient. CSS-Tricks suggests it when the native fill pseudo-element grows in a way that looks wrong next to a round thumb.

A standard fill is on the way. The CSS Form Control Styling Module Level 1 editor’s draft (8 October 2026) defines ::slider-thumb, ::slider-track and ::slider-fill for input type range, with the fill nested inside the track.

The draft describes itself as provided for discussion only and subject to change. Keep the gradient until browser support is confirmed.

Show the value with output

The output element has been Baseline widely available since October 2018, according to MDN, so it needs no fallback.

Set its for attribute to the slider’s id, which marks the slider as the source of the value. Many browsers expose output as an aria-live region, so screen readers announce updates without moving focus. The output’s value is not sent with the form, though, so the range input has to carry the data.

Update it from the input event, not change. Input fires on every movement while the user drags, whereas change fires once on release.

A tooltip bubble reuses the same fraction. Position it with the same calc() so it follows the thumb center, and borrow the arrow and fade from standard CSS tooltip patterns.

The same draft adds control-value(), which would print the slider’s value from CSS alone, for example as pseudo-element content. The draft marks it as not ready for implementation.

Can a range slider have two handles?

Not natively. A range input has one thumb, so a two-handle slider needs either two overlapping inputs or a library.

The overlap technique starts with two input type=”range” elements stacked in one container, one for the minimum and one for the maximum. Give both the same fixed min and max, and never retarget one input’s bounds to the other’s value. Then set pointer-events: none on each input and pointer-events: auto on the thumb pseudo-elements.

That last step is not optional. Developer Mike Jolley found in 2019 that with events left on, only one of the two inputs responds to clicks.

Avoid restoring pointer events on the whole input when it gains focus, since the focused input would then cover the other thumb.

Where it breaks is mostly predictable. Nothing stops the lower thumb from passing the upper one, and the two can also land on the same value, so clamp both in script.

With pointer events off the track, a click between the thumbs does nothing either. WCAG 2.2 SC 2.5.7 requires a way to set the value without dragging, and W3C accepts a text input beside the slider as that alternative, so add two number fields.

The fill gets harder too. The gradient takes a from and a to fraction, each computed with the same thumb-travel calculation.

noUiSlider is a vanilla JavaScript option. Its documentation lists ARIA support, keyboard support, multi-touch and no dependencies, and a two-handle slider takes a start array such as [20, 80] with connect: true.

React projects have Radix UI Slider and Base UI Slider, both built for multiple thumbs. Base UI exposes collision behaviors of push, swap or none, so the crossing bug becomes a setting.

Overlapping inputs keep native keyboard and screen reader behavior on each thumb, need no dependency and leave styling entirely in your hands. The cost is that you write the clamping, the fill and the click alternative yourself. Users also hit two tab stops and hear two announced values for what is logically one range, and the per-engine pseudo-element rules get written twice.

A library flips that. Collision handling, tooltips and tick scales come built in, and keyboard and ARIA support are handled for you. In return you ship and update another script and stylesheet, you override its CSS for a custom look, and accessibility depends on the library’s implementation, so test it with a screen reader.

For a price filter on a product page, a library saves the clamping and click work. For a settings panel with one range pair, overlapping inputs are enough.

How to keep a custom range slider accessible

Keep the native input and put back any focus ring you remove. Past that, W3C sets thresholds for hit area, contrast and non-drag operation, and all of them apply to sliders directly.

  • The target should be at least 24 by 24 CSS pixels (WCAG 2.2 SC 2.5.8, Level AA).
  • Components and their states need 3:1 contrast against adjacent colors (non-text contrast, SC 1.4.11, Level AA).
  • A single-pointer way to operate the control without dragging has to exist (SC 2.5.7, Level AA).
  • On the keyboard, arrows change one step, Home and End jump to the first and last values, and Page Up and Page Down take larger steps (ARIA Authoring Practices Guide).

The WCAG figures come from W3C’s Understanding documents. The key list comes from the APG slider pattern.

On hit area, W3C’s Understanding document does not single out sliders, so the safe reading is a thumb of at least 24 by 24 pixels. The user agent exception only applies while the author leaves the browser’s default size alone, so a thin custom input has to pass on spacing instead.

Contrast works on the same logic. The thumb position tells the user the value, so the thumb needs 3:1 against the track, and the track should reach 3:1 against the page so the control can be found. W3C’s non-text contrast page names the slider thumb as state information.

Style :focus-visible on the thumb with an indicator that meets the same ratio. Removing the outline without a replacement is a documented failure, listed as technique F78 against SC 1.4.11, 2.4.7 and 2.4.13.

A visible label tied to the input with for and id is the baseline for accessible forms. When the number alone means little, such as a day of the week, aria-valuetext carries the readable string, for example “Monday”.

If you rebuild the slider from a div with role=”slider”, it needs the right ARIA properties (aria-valuenow, aria-valuemin, aria-valuemax), a label, and key handlers for every key in the list above. The native input provides all of it, which is the main reason not to rebuild.

The APG also warns that some touch-based assistive technologies may not operate widgets built to its slider pattern, since they rely on synthesized key events. Test any rebuilt slider on a touch device with a screen reader before shipping.

Which approach fits a range slider?

Start with accent-color, move to pseudo-element styling when the design needs a different size or shape, and bring in a library only for multiple thumbs, tick scales or non-linear ranges.

ApproachControlsMain limitBest fit
accent-colorThumb and fill colorColor onlyBrand color on a standard slider
Pseudo-element stylingSize, shape, track, fillOne rule set per engine, gradient fillCustom look, single thumb
Framework classPrebuilt track and thumbThe framework’s own variablesProjects already on Bootstrap
LibraryHandles, tooltips, tick scalesScript and stylesheet to maintainDual handles, non-linear scales

The form-range class in Bootstrap’s form components styles the thumb and track to look the same across browsers. Bootstrap 5.3 defaults to a .5rem track and a 1rem thumb, both adjustable through Sass variables.

Tailwind projects hit the same boundary. Preline UI’s range slider docs style the native input and send tooltips, pips and multi-handle ranges to a separate plugin, and the setup relies on the Tailwind forms plugin by default.

Native styling has no way to add a second thumb. Non-linear scales are out too, because a native step moves in equal increments (MDN), while noUiSlider accepts non-linear ranges, which suits values spread from 10 to 10,000.

A library for a single-thumb slider is overkill. It adds a script and a stylesheet to a control the browser already provides.

The same goes for adding Bootstrap for one slider. Use the framework class only when the framework is already on the page.

CSS Range Slider FAQ

Is a range slider better than a number input?

Only when the exact value does not matter. A slider suits volume or brightness, where position carries the meaning, while a number input suits quantities a user has to type precisely.

The two also pair well. W3C accepts a text input beside a slider as the non-drag alternative under WCAG 2.2.

Do the ::-ms- pseudo-elements still matter?

Not for new projects. Rules such as ::-ms-thumb and ::-ms-fill-lower only worked in Internet Explorer and legacy Edge, and Chromium-based Edge reads the ::-webkit- rules instead.

Add them only if your supported browser list still includes those engines.

How do you make a vertical range slider?

Set writing-mode: vertical-lr on the input, then add direction: rtl so the minimum sits at the bottom. MDN notes that vertical form controls only gained full browser support in 2024. For the range input, its compatibility data lists Chrome 124, Firefox 120 and Safari 16.5, so check that table for older targets.

The slider-orientation property in the CSS Form Control Styling draft proposes a one-line alternative, but it remains a draft.

How do you add tick marks to a range slider with a datalist?

Point the input’s list attribute at a datalist, then add option elements whose values fall between min and max. Each option can carry a label attribute for the text under the tick.

Browsers differ in how they draw the ticks and labels. If the look must match everywhere, paint the ticks yourself with a repeating-linear-gradient on the track.

What to Test Before a CSS Range Slider Goes Live

A CSS range slider needs testing in Chrome, Firefox and Safari, plus a touch device with a screen reader. Chrome and Safari read one set of thumb rules, and Firefox reads another.

Check Firefox next to Chrome first, since that’s where the rules diverge. Then go keyboard only with the arrows, Home and End, and finish on a physical phone with a screen reader running.

The order follows cost. Engine differences show on screen in seconds, keyboard checks take a minute, and touch assistive technology needs a real device.

As of October 2026, the vendor-prefixed rules stay until ::slider-thumb, ::slider-track and ::slider-fill ship across engines. The CSS Working Group places unprefixed implementations at the Candidate Recommendation stage, and the module is still a Working Draft.

Until then, a native, accessible slider costs two rule sets to maintain.