GrSlider
Pick a number or a range by dragging a thumb, with steps, marks and keyboard support.
Machine-translated from the Russian original, not yet reviewed. Read the original
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 |
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.
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 and the labels of the marks with the --gr-disabled-* tokens
rather than with transparency: opacity dilutes text colours tuned to AA.
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.
Playground 13
Loading…
<GrSlider />Install
npm i @feugene/granularityImport
import { GrSlider } from '@feugene/granularity/components/GrSlider'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | — |
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 | — |
ariaLabel | string | undefined | undefined | — |
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 | — |
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 | — |
min | number | undefined | 0 | — |
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`. |
modelValuerequired | GrSliderModelValue | — | `number` is a single value; `[lo, hi]` is a range (with `range=true`). |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: GrSliderModelValue] | — |
change | [value: GrSliderModelValue] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Description |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Examples 4
Single value with tooltip
Value: 40 — drag the thumb or use arrow keys, Home / End, PageUp / PageDown.
<script setup lang="ts">
import { ref } from 'vue'
import { GrSlider } from '@feugene/granularity'
const volume = ref(40)
</script>
<template>
<div class="grid gap-4">
<GrSlider
v-model="volume"
:min="0"
:max="100"
show-tooltip
:format-tooltip="(v) => `${v}%`"
aria-label="Volume"
/>
<p class="text-sm text-[var(--gr-muted-fg)]">
Value: <code>{{ volume }}</code> — drag the thumb or use arrow keys, Home / End, PageUp / PageDown.
</p>
</div>
</template>Range with two thumbs
From $200 to $700 — two thumbs that never cross.
<script setup lang="ts">
import { ref } from 'vue'
import { GrSlider } from '@feugene/granularity'
const price = ref<[number, number]>([200, 700])
</script>
<template>
<div class="grid gap-4">
<GrSlider
v-model="price"
range
:min="0"
:max="1000"
:step="50"
show-tooltip="always"
:format-tooltip="(v) => `$${v}`"
aria-label="Price range"
/>
<p class="text-sm text-[var(--gr-muted-fg)]">
From <code>${{ price[0] }}</code> to <code>${{ price[1] }}</code> — two thumbs that never cross.
</p>
</div>
</template>Marks, steps, sizes and disabled
<script setup lang="ts">
import { ref } from 'vue'
import { GrSlider } from '@feugene/granularity'
const quality = ref(50)
const marks = {
0: 'Low',
25: 'Fair',
50: 'Good',
75: 'High',
100: 'Max',
}
</script>
<template>
<div class="grid gap-8">
<GrSlider
v-model="quality"
:min="0"
:max="100"
:step="25"
:marks="marks"
aria-label="Quality"
/>
<div class="grid gap-6">
<GrSlider :model-value="30" size="sm" aria-label="Small" />
<GrSlider :model-value="60" size="lg" disabled aria-label="Large disabled" />
</div>
</div>
</template>Custom colors & size (CSS variables)
<script setup lang="ts">
import { ref } from 'vue'
import { GrSlider } from '@feugene/granularity'
const brand = ref(65)
const accent = ref(40)
const large = ref(70)
const volume = ref(35)
const budget = ref(1200)
</script>
<template>
<div class="grid gap-8">
<!-- Свой цвет заливки + подложка дорожки. -->
<GrSlider
v-model="brand"
aria-label="Brand color"
show-tooltip
:style="{
'--gr-slider-fill': '#8b5cf6',
'--gr-slider-rail': 'color-mix(in srgb, #8b5cf6 20%, transparent)',
}"
/>
<!-- Сплошной бегунок: заливка = цвет, окантовка = фон. -->
<GrSlider
v-model="accent"
aria-label="Accent"
:style="{
'--gr-slider-fill': '#f97316',
'--gr-slider-thumb-bg': '#f97316',
'--gr-slider-thumb-border': 'var(--gr-bg)',
}"
/>
<!-- Крупнее бегунок и толще дорожка. -->
<GrSlider
v-model="large"
aria-label="Large"
:style="{
'--gr-slider-fill': 'var(--gr-success)',
'--gr-slider-thumb-size': '1.5rem',
'--gr-slider-track-height': '0.75rem',
}"
/>
<div class="flex items-start gap-10">
<!-- Вертикальная дорожка: минимум внизу, длина — через --gr-slider-length. -->
<GrSlider
v-model="volume"
orientation="vertical"
aria-label="Volume"
show-tooltip
:marks="{ 0: 'Mute', 50: 'Half', 100: 'Max' }"
:style="{ '--gr-slider-length': '12rem' }"
/>
<!-- lazy: значение уезжает наружу только на отпускании. -->
<div class="grid flex-1 gap-2">
<GrSlider
v-model="budget"
lazy
:min="0"
:max="5000"
:step="50"
aria-label="Monthly budget"
show-tooltip
:format-tooltip="(value) => `$${value.toLocaleString('en-US')}`"
/>
<div class="text-sm text-[var(--gr-muted-fg)]">
Committed value: <span class="font-medium text-[var(--gr-fg)]">${{ budget.toLocaleString('en-US') }}</span>
</div>
</div>
</div>
</div>
</template>Accessibility
- APG pattern
slider