Skip to content

PlSlider

A value chosen along a range. The rail is a neutral groove and the run that fills it is the same gradient a button is made of.

The rail is --plass-track, the same ink a PlSwitch's off state is. It is not the glass with an inset shadow in it, which is what a filled field is: a field is a box you look into, and a rail is a line you look along, and the part of a rail that matters is the part with nothing on it, which is exactly the part a white-on-white groove does not have.

React
tsx
import { PlSlider } from 'plass-ui';

<PlSlider label="Volume" value={volume} onValueChange={setVolume} showValue />;
dart
import 'package:plass_ui/plass_ui.dart';

PlSlider(
  label: const Text('Volume'),
  values: <double>[volume],
  showValue: true,
  onChanged: (List<double> next) => setState(() => volume = next.first),
);

Props

PropTypeDefaultDescription
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Groove thickness, thumb diameter, and the label type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'The gradient of the filled run, and the thumb on it
elevationshared0 | 1 | 2 | 31Drop shadow depth of the thumb. It is the part you press, so it takes a control default of 1
orientationshared'horizontal' | 'vertical''horizontal'Which way the slider runs. A vertical slider has no length of its own, so give it a height
valuenumber | number[]The current value. An array makes it a range slider with one thumb per entry
defaultValuenumber | number[]The starting value, uncontrolled
onValueChange(value: number | number[]) => voidCalled with the new value
min · max · stepnumber0 · 100 · 1The range and its increments, passed straight to Base UI
labelReactNodeThe label above the track
descriptionReactNodeHelper text below the track
showValueboolean | ((formatted, values) => ReactNode)falseShows the current value beside the label. Pass a function to format it
disabledbooleanfalseUnavailable. Loses its saturation, lets the page through, and leaves the tab order
namestringIdentifies the control when a form is submitted
PropTypeDefaultDescription
values * List<double>The chosen value, or the ends of the chosen range. Always a list: the length is what makes it a range
onChangedValueChanged<List<double>>?Called with the new value
onChangeEndValueChanged<List<double>>?Called once, when the thumb is let go
min · max · stepdouble0 · 100 · 1The range and its increments, passed straight to Base UI
sizesharedPlassSizePlassSize.mdGroove thickness, thumb diameter, and the label type scale
colorsharedPlassColorPlassColor.primaryThe gradient of the filled run, and the thumb on it
elevationsharedint1Drop shadow depth of the thumb. It is the part you press, so it takes a control default of 1
orientationsharedPlassOrientationPlassOrientation.horizontalWhich way the slider runs. A vertical slider has no length of its own, so give it a height
lengthdouble?How long the run is. A vertical slider has no length of its own, so this is where one comes from — 160 by default
labelWidget?The label above the track
descriptionWidget?Helper text below the track
showValueboolfalseShows the current value beside the label. Pass a function to format it
formatValueString Function(List<double>)?Formats that value. Left out, it is printed with no decimals and joined with an en dash
disabledboolfalseUnavailable. Loses its saturation, lets the page through, and leaves the tab order
semanticLabelString?The name a screen reader announces, for a slider with no visible label

Every other prop on Base UI's Slider.Root passes straight through, minStepsBetweenValues, largeStep, format, onValueCommitted, name, disabled.

values is always a list, even for a single value: it is the same parameter either way, and the length is what makes it a range.

There is no variant here. The three materials answer "what is this surface made of", and a slider is two surfaces at once: a groove and a key travelling along it. Neither has a choice to offer.

The thumb travels to a value it was not dragged to: an arrow key, a press on the rail, or a value set from elsewhere. It moves over the same duration everything else here does, and the run behind it fills at the same rate. Under a finger it does not travel at all, because a thumb that eased towards the pointer would lag behind it. This is the one place in the library a position is animated, and it keeps the no-transform rule: what moves is the value, not the control.

What the shared axes (size color elevation orientation) mean across the library is in prop conventions.

Examples

Range

Pass more than one value and it becomes a range slider with one thumb per entry. There is no separate range prop, because the shape of the value already says which one this is.

The thumbs cannot cross: a value is held between its neighbours, so a range whose ends have swapped is a range that was entered backwards, and the fix lives here rather than in every caller.

React

color

The filled run is the family's gradient, the same two-stop sweep at 135° a solid button carries, and the thumb sits on it, ringed in the page's own surface colour so it never dissolves into the run behind it.

React

min · max · step

step decides what the thumb can land on. A slider with five stops is still a slider and not a segmented control: it is chosen by dragging, and the values are on a scale.

React

showValue

true prints the raw value; a function is handed both Base UI's already-localised strings and the raw numbers, so a currency, a percentage or a duration is one line.

showValue turns the number on and formatValue decides what it says, a currency, a percentage, a duration. Left out, the values are printed with no decimals and joined with an en dash.

The value sits at the end of the label's row rather than following the thumb. A number that moves is a number that is hard to read and impossible to compare between two sliders stacked on each other.

size

Moves the groove, the thumb and the label together. The thumb is deliberately far bigger than the groove at every step. It is the only part of the control you can actually catch, and a thumb sized to match a 6px rail is a thumb nobody hits on a touchscreen.

React

orientation

A vertical slider has no length of its own, so it is given one: 160px by default. Override it with a class`length` overrides it when a mixer needs taller faders.

React

disabled

The light going out, as everywhere else: the shape and the position stay, the saturation and half the opacity go.

React

Accessibility

  • Each thumb is a real <input type="range">, so the browser's own slider semantics, the tab order and disabled all come for free.
  • label is wired to the control by Base UI. Without one, a fader in a bank of them, give the slider an aria-label.
  • The keyboard is the primitive's: step, PageUp / PageDown take the large step, Home and End jump to the ends.
  • The whole strip is a pointer target, not just the rail: the control box is several times the groove's thickness, so a press anywhere along it moves the thumb.
  • The thumb grows a halo on hover and while dragging rather than growing itself. Nothing under the finger is ever scaled.
  • showValue is a rendered number, not a substitute for the accessible value. That is aria-valuenow on the input, which Base UI keeps in step.
  • Announced as a slider, with the current value as its value. Without a visible label, a fader in a bank of them, give it a semanticLabel.
  • move a thumb by one step, PageUp / PageDown by a tenth of the range, and Home and End jump to the ends.
  • Each thumb is its own focus stop, which is what makes a range slider operable: Tab moves between the two ends.
  • The whole strip is a pointer target, not just the rail: the control box is several times the groove's thickness, so a press anywhere along it moves the nearest thumb.
  • The thumb grows a halo on hover and while dragging rather than growing itself. Nothing under the finger is ever scaled.
  • showValue is a drawn number, not a substitute for the announced one.

Differences from the React build

ReactFlutterWhy
value as a number or an arrayvalues, always a listOne parameter either way, and the length is what makes it a range.
onValueChange / onValueCommittedonChanged / onChangeEndFlutter's names for "as it moves" and "when it is let go".
showValue as boolean-or-functionshowValue and formatValueDart has no union type, so turning the number on and deciding what it says are two parameters.
<input type="range">a drawn strip with its own key handlingThere is no native range input to inherit a keyboard from, so the keys are bound here, the same set, including Page and Home/End.
aria-labelsemanticLabelFlutter's name.
className for a vertical slider's heightlengthThere is no class list; the length is a parameter.

Released under the MIT License