GrSlider

  • core
  • forms
  • API · 17 props
  • ⌨ keys

Pick a number or a range by dragging a thumb, with steps, marks and keyboard support.

import { GrSlider } from '@feugene/granularity/components/GrSlider'

Overview

When to take it

  • an exact number does not matter — volume, brightness, opacity: “more or less” matters more;
  • the range is illustrative — min/max show the bounds, which are invisible in an input field;
  • a “from — to” range is needed — range gives two thumbs that do not pass through each other;
  • the scale has reference points — marks label the steps;
  • the value is applied on release — lazy does not send a request on every movement.

When to take something else

NeedTake
An exact number is neededGrNumberInput
A rating in starsGrRating
There are few steps and they are namedGrSegmented / GrRadioGroup
The value is shown rather than setGrProgressBar

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • A price filter in a catalogue

    range gives two thumbs that never pass each other, and a click on the track moves the nearer one. lazy holds the model until the thumb is let go — the catalogue asks once per gesture, not on every pixel — and change reports it. marks put the price points on the track; the mark slot colours the ones inside the range with active, 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-length sets the height. The thumb slot writes the degrees inside a larger knob — it fills the thumb rather than replacing it, so the role, the keys and the aria-* stay the component’s, and marks step aside by the size of that knob. A room on schedule is readonly: the value is visible, the building owns it.

    21°
    LisbonUp to 8 people21 °C · comfortable
    19°
    PortoUp to 4 people19 °C · comfortable
  • Exporting 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 sets invalid with a reason. show-tooltip="always" keeps the number in sight, format-tooltip gives the same text to aria-valuetext, an array of marks and a step snap the width to common sizes, and name posts both values.

    Export «oak-desk-front.tif»About 171 KB

    Lower is smaller and blurrier

    80%

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseUnavailable: the thumbs neither move nor take focus. It adds up by "or" with the field and the form.
readonlyboolean | undefinedfalseRead only: the value is visible but does not change.
invalidboolean | undefinedfalseThe visual and ARIA state of an error.
requiredboolean | undefinedfalseA mandatory field (`aria-required`).
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe thickness of the track and the size of the thumb. `md` by default; taken from `GrConfigProvider`.
ariaLabelstring | undefinedundefinedThe accessible name outside `GrFormField`. In a range the thumbs get it with a "min"/"max" mark.
namestring | undefinedundefinedThe name for a native form: a hidden input for the value, and with `range` two of them under one name.
maxnumber | undefined100The 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.
lazyboolean | undefinedfalseEmit `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.
stepnumber | undefined1The step the value snaps to — and the step of the arrow keys. `1` by default.
minnumber | undefined0The lower bound of the scale; a value outside the bounds is clamped to it. `0` by default.
rangeboolean | undefinedfalseA range with two thumbs; the model is a `[lo, hi]` tuple.
marksGrSliderMarks | undefinedundefinedThe marks of the steps: `{ [value]: label }` or an array of values.

Slots

SlotTypeDescription
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
{ index: number; value: number; percent: number; active: boo{ index: number; value: number; percent: number; active: boolean; disabled: boolean; }
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

EventSignatureDescription
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

MethodsSignatureDescription
focus() => voidPut focus on the thumb; in a range — on the lower one.
blur() => voidRemove 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
import type { GrSliderConfigurableProps, GrSliderSize, GrSliderModelValue } from '@feugene/granularity/components/GrSlider'

Filedist/types/components/GrSlider/config.d.ts

grSliderConfigconst
const grSliderConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSlider">;

Filedist/types/components/GrSlider/defaults.d.ts

GrSliderConfigurablePropsinterface
interface GrSliderConfigurableProps {
    size: GrSliderSize;
}

Filedist/types/components/GrSlider/grSliderStyles.d.ts

GrSliderSizetype
type GrSliderSize = GrComponentSize;
GrSliderModelValuetype
type GrSliderModelValue = number | [number, number];
GrSliderMarkstype
type GrSliderMarks = Record<number, string> | number[];

Filedist/types/components/GrSlider/GrSlider.vue.d.ts

GrSliderPropsinterface
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;
GrSliderEmitsinterface
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

grSliderSafelistconst
const grSliderSafelist: string[];

Filedist/types/components/GrSlider/index.d.ts

GrSliderInstancetype
type GrSliderInstance = ComponentExposed<typeof GrSliderComponent>;

Accessibility

APG pattern

slider

Keys

← / ↓→ / ↑PageUp / PageDownHome / End

Usage from the library prose

The slots fill the track rather than replace it
vue
<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
vue
<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
vue
<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.