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.
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.
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.
"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, aFieldLabeloraria-labelon aSliderThumb. In a range, name each end. - The focus ring goes around the thumb whose input has focus.
API
Slider
| Prop | Type | Default | Description |
|---|---|---|---|
thumbLabel | string | string[] | — | Names the thumbs the slider draws: one name, or one per thumb of a range. |
ticks | boolean | number | — | Tick marks under the track: true for one per step, or a count. |
size | "sm" | "md" | "md" | Thumb and track size. |
value | number | number[] | — | The value. Pass it to control the slider. |
defaultValue | number | 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. |
min | number | 0 | The lowest value. |
max | number | 100 | The highest value. |
step | number | 1 | How far one step moves. |
minStepsBetweenThumbs | number | 0 | How many steps apart a range's thumbs stay. |
orientation | "horizontal" | "vertical" | "horizontal" | The axis. Vertical grows upward. |
format | Intl.NumberFormatOptions | (value) => string | — | How values read, on screen and to screen readers. |
disabled | boolean | false | Can'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
| Attribute | On | Values |
|---|---|---|
data-size | Slider | sm, md |
data-orientation | Every part | horizontal, vertical |
data-dragging | Every part | Present while a thumb is dragged |
data-index | SliderThumb | Which value the thumb shows |
data-disabled | Every part | Present when disabled |
CSS variables
| Variable | macOS | iOS | What it sets |
|---|---|---|---|
--mb-slider-track | 4px | 6px | Track thickness |
--mb-slider-thumb | 20px | 28px | Thumb diameter |
--mb-slider-track-sm | 3px | 4px | Small track |
--mb-slider-thumb-sm | 14px | 22px | Small thumb |
--mb-slider-thumb-grow | 1 | 1.12 | Thumb scale while dragged |
--mb-slider-tick | 6px | 8px | Tick mark length |