macbetter

Buttons and controls

Slider

Choose a value, or a range, from a continuous or stepped scale.

import { Slider } from "macbetter/slider"
import { Volume1Icon, Volume2Icon } from "lucide-react"

export default function Example() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", maxWidth: 360 }}>
      <Volume1Icon aria-hidden="true" size={16} style={{ color: "var(--mb-label-secondary)", flex: "none" }} />
      <Slider defaultValue={60} thumbLabel="Volume" />
      <Volume2Icon aria-hidden="true" size={16} style={{ color: "var(--mb-label-secondary)", flex: "none" }} />
    </div>
  )
}

When to use it

Use a slider when the exact number matters less than the position, like volume, brightness or a price range, and when seeing the effect while dragging helps. When people need a precise number, pair the slider with a Number Field or use one instead.

  • Put icons or labels at the ends that say what each end means, like a small and a large speaker.
  • Fill the track from the minimum with the accent, so the value reads at a glance.
  • Use tick marks only when there are a few meaningful steps, like a key repeat rate.

Usage

import { Slider, SliderControl, SliderValue } from "macbetter/slider"

Slider draws its own track, fill and thumb. Name the thumb with thumbLabel, or put the slider in a Field whose label names it.

With a label and value

Compose the parts to place a FieldLabel and a SliderValue above the track. format takes Intl.NumberFormat options and sets both the shown value and what screen readers hear.

80%
import { Field, FieldLabel } from "macbetter/field"
import { Slider, SliderControl, SliderValue } from "macbetter/slider"

export default function Example() {
  return (
    <Field style={{ width: "100%", maxWidth: 360 }}>
      <Slider defaultValue={0.8} min={0} max={1} step={0.01} format={{ style: "percent" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <FieldLabel>Brightness</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl />
      </Slider>
    </Field>
  )
}

Range

Pass an array for a range, and the slider draws a thumb per value with the accent fill between them. Name each thumb.

$120 – $340
import { Field, FieldLabel } from "macbetter/field"
import { Slider, SliderControl, SliderValue } from "macbetter/slider"

export default function Example() {
  return (
    <Field style={{ width: "100%", maxWidth: 360 }}>
      <Slider
        defaultValue={[120, 340]}
        min={0}
        max={500}
        step={10}
        minStepsBetweenThumbs={2}
        format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      >
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <FieldLabel>Price per night</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl thumbLabel={["Minimum price", "Maximum price"]} />
      </Slider>
    </Field>
  )
}

Tick marks

ticks draws marks under the track: true puts one at every step, or pass a count. The thumb snaps to step.

import { Slider } from "macbetter/slider"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6, width: "100%", maxWidth: 360 }}>
      <Slider defaultValue={3} min={1} max={5} step={1} ticks thumbLabel="Key repeat rate" />
      <div
        aria-hidden="true"
        style={{ display: "flex", justifyContent: "space-between", color: "var(--mb-label-secondary)", font: "var(--mb-text-caption)" }}
      >
        <span>Slow</span>
        <span>Fast</span>
      </div>
    </div>
  )
}

Vertical

orientation="vertical" runs the slider bottom to top, 160 points tall by default. Set a height to change it.

import { Slider } from "macbetter/slider"

const bands = [
  { label: "60 Hz", value: 62 },
  { label: "250 Hz", value: 48 },
  { label: "1 kHz", value: 55 },
  { label: "4 kHz", value: 70 },
  { label: "16 kHz", value: 40 },
]

export default function Example() {
  return (
    <div style={{ display: "flex", gap: 12 }}>
      {bands.map((band) => (
        <div key={band.label} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
          <Slider orientation="vertical" defaultValue={band.value} thumbLabel={band.label} />
          <span aria-hidden="true" style={{ color: "var(--mb-label-secondary)", font: "var(--mb-text-caption)" }}>
            {band.label}
          </span>
        </div>
      ))}
    </div>
  )
}

Sizes

size="sm" is a smaller thumb and thinner track for inspectors.

import { Slider } from "macbetter/slider"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16, width: "100%", maxWidth: 360 }}>
      <Slider size="sm" defaultValue={30} thumbLabel="Icon size" />
      <Slider size="md" defaultValue={70} thumbLabel="Text size" />
    </div>
  )
}

Disabled

import { Slider } from "macbetter/slider"

export default function Example() {
  return (
    <div style={{ width: "100%", maxWidth: 360 }}>
      <Slider disabled defaultValue={40} thumbLabel="Microphone volume" />
    </div>
  )
}

Controlled

Pass value and onValueChange, which runs on every step of a drag. Use onValueCommit to save once the drag ends.

25 min
"use client"

import { Slider } from "macbetter/slider"
import { useState } from "react"

export default function Example() {
  const [minutes, setMinutes] = useState(25)

  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, width: "100%", maxWidth: 360 }}>
      <span style={{ font: "var(--mb-text-title-2)", fontVariantNumeric: "tabular-nums" }}>{minutes} min</span>
      <Slider
        value={minutes}
        onValueChange={(value) => setMinutes(value as number)}
        min={5}
        max={60}
        step={5}
        thumbLabel="Focus timer length"
      />
    </div>
  )
}

Platform behavior

  • macOS. A thin, 4-point track and a 20-point white knob with a soft shadow. The knob dims slightly while held, as AppKit's does.
  • iPadOS and iOS. A 6-point track and the 28-point UISlider thumb, which grows a little under the finger while you drag.
  • Every platform. The track is inset by half a thumb, so the thumb never hangs past the ends, and the control is at least as tall as the platform's touch target. Tick marks line up with the steps they mark.

Accessibility

  • Each thumb is a native range input, so the arrow keys, Page Up and Page Down, Home and End work, and screen readers on phones adjust it with their own gestures.
  • Name every thumb, with thumbLabel, a FieldLabel or aria-label on a SliderThumb. In a range, name each end.
  • The focus ring goes around the thumb whose input has focus.

API

Slider

PropTypeDefaultDescription
thumbLabelstring | string[]—Names the thumbs the slider draws: one name, or one per thumb of a range.
ticksboolean | number—Tick marks under the track: true for one per step, or a count.
size"sm" | "md""md"Thumb and track size.
valuenumber | number[]—The value. Pass it to control the slider.
defaultValuenumber | number[]—The value at first. An array makes a range.
onValueChange(value, details) => void—Runs on every step of a drag or a key.
onValueCommit(value, details) => void—Runs once an interaction ends. For saving.
minnumber0The lowest value.
maxnumber100The highest value.
stepnumber1How far one step moves.
minStepsBetweenThumbsnumber0How many steps apart a range's thumbs stay.
orientation"horizontal" | "vertical""horizontal"The axis. Vertical grows upward.
formatIntl.NumberFormatOptions | (value) => string—How values read, on screen and to screen readers.
disabledbooleanfalseCan't be focused or moved.

SliderControl

Draws the track, the accent fill and one thumb per value when it has no children. Takes thumbLabel and ticks (a count).

Other parts

SliderValue shows the formatted value. SliderTrack, SliderIndicator, SliderThumb and SliderTicks are exported for a slider you compose yourself.

Data attributes

AttributeOnValues
data-sizeSlidersm, md
data-orientationEvery parthorizontal, vertical
data-draggingEvery partPresent while a thumb is dragged
data-indexSliderThumbWhich value the thumb shows
data-disabledEvery partPresent when disabled

CSS variables

VariablemacOSiOSWhat it sets
--mb-slider-track4px6pxTrack thickness
--mb-slider-thumb20px28pxThumb diameter
--mb-slider-track-sm3px4pxSmall track
--mb-slider-thumb-sm14px22pxSmall thumb
--mb-slider-thumb-grow11.12Thumb scale while dragged
--mb-slider-tick6px8pxTick mark length