GrSlider
Pick a number or a range by dragging a thumb, with steps, marks and keyboard support.
Overview
When to take it
- an exact number does not matter — volume, brightness, opacity: “more or less” matters more;
- the range is illustrative —
min/maxshow the bounds, which are invisible in an input field; - a “from — to” range is needed —
rangegives two thumbs that do not pass through each other; - the scale has reference points —
markslabel the steps; - the value is applied on release —
lazydoes not send a request on every movement.
When to take something else
| Need | Take |
|---|---|
| An exact number is needed | GrNumberInput |
| A rating in stars | GrRating |
| There are few steps and they are named | GrSegmented / GrRadioGroup |
| The value is shown rather than set | GrProgressBar |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrSlider
v-model="value"
showTooltip
ariaLabel="Notification volume"
/>Examples 3 each with a live demo and its code
A price filter in a catalogue
rangegives two thumbs that never pass each other, and a click on the track moves the nearer one.lazyholds the model until the thumb is let go — the catalogue asks once per gesture, not on every pixel — andchangereports it.marksput the price points on the track; themarkslot colours the ones inside the range withactive, counted the way the fill is.Price€100 – €500 Drag a thumb, or focus it and use the arrow keys- Tromsø compact desk€129
- Oslo writing desk€149
- Kalmar corner desk€219
- Uppsala desk with drawers€265
- Bergen standing desk€389
- Lund oak desk€459
Thermostats of meeting rooms
A thermostat stands upright:
orientation="vertical"puts the minimum at the bottom, and--gr-slider-lengthsets the height. Thethumbslot writes the degrees inside a larger knob — it fills the thumb rather than replacing it, so the role, the keys and thearia-*stay the component’s, andmarksstep aside by the size of that knob. A room on schedule isreadonly: the value is visible, the building owns it.LisbonUp to 8 people21 °C · comfortablePortoUp to 4 people19 °C · comfortableExporting a product photo
PNG has no quality to set, so the slider is
disabled; below 40 the price tag on the photo breaks up, and the page setsinvalidwith a reason.show-tooltip="always"keeps the number in sight,format-tooltipgives the same text toaria-valuetext, an array ofmarksand astepsnap the width to common sizes, andnameposts both values.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Unavailable: the thumbs neither move nor take focus. It adds up by "or" with the field and the form. |
readonly | boolean | undefined | false | Read only: the value is visible but does not change. |
invalid | boolean | undefined | false | The visual and ARIA state of an error. |
required | boolean | undefined | false | A mandatory field (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The thickness of the track and the size of the thumb. `md` by default; taken from `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`. In a range the thumbs get it with a "min"/"max" mark. |
name | string | undefined | undefined | The name for a native form: a hidden input for the value, and with `range` two of them under one name. |
max | number | undefined | 100 | The upper bound of the scale. `100` by default. |
orientation | "horizontal" | "vertical" | undefined | "horizontal" | The orientation of the track. In the vertical one the minimum is at the bottom. |
lazy | boolean | undefined | false | Emit `update:modelValue` only at the end of the gesture: during the drag the value is run by the slider itself. The keyboard commits at once — a key press is discrete, and there is nothing to hold back. |
step | number | undefined | 1 | The step the value snaps to — and the step of the arrow keys. `1` by default. |
min | number | undefined | 0 | The lower bound of the scale; a value outside the bounds is clamped to it. `0` by default. |
range | boolean | undefined | false | A range with two thumbs; the model is a `[lo, hi]` tuple. |
marks | GrSliderMarks | undefined | undefined | The marks of the steps: `{ [value]: label }` or an array of values. |
showTooltip | boolean | "hover" | "always" | undefined | false | A popping value above the thumb: `true`/`'hover'` — on hover, drag and focus, `'always'` — always. |
formatTooltip | ((value: number) => string) | undefined | undefined | The formatting of the value in the tooltip. The same goes into `aria-valuetext`. |
modelValuerequiredv-model | GrSliderModelValue | — | `number` is a single value; `[lo, hi]` is a range (with `range=true`). |
Slots
| Slot | Type | Description |
|---|---|---|
mark | { value: number; label: string; percent: number; active: boolean; } | The label of a scale mark. The slot fills its node rather than replacing it: the position, `aria-hidden` and the duplication of the value for the screen reader stay with the component. |
thumb |
| The content of the thumb. The slot is rendered **inside** the handle rather than instead of it: `role="slider"`, `tabindex`, eight `aria-*` and the positioning hang on the node itself, and handing it over to the consumer’s markup would mean handing over the whole widget contract along with it. The look of the handle itself is changed with the `--gr-slider-thumb-*` tokens. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: GrSliderModelValue): void | — |
change | (event: "change", value: GrSliderModelValue): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
focus | () => void | Put focus on the thumb; in a range — on the lower one. |
blur | () => void | Remove focus from the first thumb. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrSliderConfigurableProps, GrSliderSize, GrSliderModelValue } from '@feugene/granularity/components/GrSlider'Filedist/types/components/GrSlider/config.d.ts
const grSliderConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSlider">;Filedist/types/components/GrSlider/defaults.d.ts
interface GrSliderConfigurableProps {
size: GrSliderSize;
}Filedist/types/components/GrSlider/grSliderStyles.d.ts
type GrSliderSize = GrComponentSize;type GrSliderModelValue = number | [number, number];type GrSliderMarks = Record<number, string> | number[];Filedist/types/components/GrSlider/GrSlider.vue.d.ts
interface GrSliderProps {
modelValue: GrSliderModelValue;
min?: number;
max?: number;
step?: number;
range?: boolean;
disabled?: boolean;
readonly?: boolean;
invalid?: boolean;
required?: boolean;
size?: GrSliderSize;
marks?: GrSliderMarks;
showTooltip?: boolean | 'always' | 'hover';
formatTooltip?: (value: number) => string;interface GrSliderEmits {
(e: 'update:modelValue', value: GrSliderModelValue): void;
(e: 'change', value: GrSliderModelValue): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
}Filedist/types/components/GrSlider/safelist.d.ts
const grSliderSafelist: string[];Filedist/types/components/GrSlider/index.d.ts
type GrSliderInstance = ComponentExposed<typeof GrSliderComponent>;Accessibility
APG pattern
slider
Keys
← / ↓→ / ↑PageUp / PageDownHome / End
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The slots fill the track rather than replace it
<GrSlider v-model="temperature" :marks="{ 16: 'cold', 30: 'hot' }">
<template #thumb="{ value }">{{ value }}</template>
<template #mark="{ label, active }">
<span :class="active ? 'text-[var(--gr-primary-text)]' : ''">{{ label }}</span>
</template>
</GrSlider>#thumb is rendered inside the thumb rather than instead of it. Its node carries
role="slider", tabindex, eight aria-* — including aria-valuemin and aria-valuemax, which
for a range are computed from the neighbouring thumb — plus the keyboard handler and the
positioning. Handing that markup to the consumer would mean handing over the whole widget contract
with it.
The look of the thumb itself is meanwhile changed with the --gr-slider-thumb-bg, -border and
-size tokens, so “a completely different thumb” is assembled from the “tokens + slot” pair. The
tooltip with the value stays beside the slot rather than being displaced by it.
#mark receives active — whether the mark has fallen into the filled part. That is what the
slot is most often taken for: it cannot be computed from the outside, the model does not reach the
scope otherwise. For a range the filled part is the stretch between the thumbs rather than from
zero, so a mark to the left of the first thumb does not count as passed — active is computed the
same way as the fill itself.
The label of a mark stays aria-hidden: a screen reader reads the value from the thumb, and a
second text inside the slider only gets in its way.
The text of the tooltip is set by formatTooltip rather than by a slot: a string is needed there,
not markup — the tooltip lives on top of the thumb and has no layout of its own.
The range
<GrSlider v-model="range" range :min="0" :max="100" aria-label="Budget" />The model is a [lo, hi] tuple; the thumbs do not jump over each other. A click on the track moves
the nearest thumb, and when both have converged into one point — the one in whose direction the
click happened: otherwise a collapsed range could not be pulled apart with the mouse.
Every thumb gets a name of its own: ariaLabel plus the bound from the locale
(gr.slider.min / gr.slider.max) — “Budget (minimum)”.
The orientation
<GrSlider v-model="volume" orientation="vertical" :style="{ '--gr-slider-length': '12rem' }" />In a vertical track the minimum is at the bottom. The length is set by --gr-slider-length
(10rem by default), and the thickness is still held by --gr-slider-track-height. The tooltip
moves to the side — above the thumb it would lie on the track — and the labels of the marks stand
on the right. aria-orientation follows the prop, and the keyboard does not change: ↑/→
increase, ↓/← decrease.
When the value goes outside
lazy holds update:modelValue back until the end of a gesture: during the drag the thumb is run
by an internal value, and one event goes out on release (together with change). That is for
expensive recomputations that have no reason to fire on every movement of the mouse.
The keyboard commits at once even with lazy: a key press is discrete, and there is nothing to
hold back.
A modelValue that did not arrive puts the thumb at min and is explained with a warning in dev
mode: otherwise the offset and aria-valuenow would go to NaN, and a screen reader would read it
aloud.
The marks and the tooltip
marks is a { [value]: label } dictionary or an array of values. The labels of the marks are
hidden from a screen reader (aria-hidden): it reads the value from the thumb itself, and the
labels would become random text inside the slider.
formatTooltip governs both the tooltip and aria-valuetext — “$1,200” instead of a bare “1200”.
Without a format of your own aria-valuetext is not set: the number is self-sufficient.
The labels of the marks are offset from the thumb rather than from the track: the offset is how
far the thumb sticks out beyond the track plus a gap, but never less than the standard one. A large
thumb (--gr-slider-thumb-size together with the #thumb slot) therefore does not cover the labels
even when it stands right on a mark, and the room under the labels grows with it.
Styling
size is xs…lg and is read from GrConfigProvider. Pointed customisation goes through the
variables: --gr-slider-rail, --gr-slider-fill, --gr-slider-thumb-bg,
--gr-slider-thumb-border, --gr-slider-thumb-size, --gr-slider-track-height,
--gr-slider-length.
disabled dims the fill, the thumb (together with its border), the tooltip and the labels of the
marks with the --gr-disabled-* tokens rather than with transparency: opacity dilutes text
colours tuned to AA.
invalid — its own or one from a GrFormField error — paints the fill and the border of the thumb
with the --gr-invalid-brd role, and the focus ring with --gr-invalid-ring, like the border of
the input fields. Being unavailable is stronger than an error.
The native form
The name prop renders an input[type="hidden"] with the value; with range there are two inputs
with one name (lo, hi). What is serialised is the model (after snapping to step) rather than
the draft of a gesture with lazy.
An unavailable slider does not send its value — like a native disabled control, GrSegmented
and GrSwitch: the hidden inputs (both of them with range) get disabled.
Props in this example