GrSlider
Берут, когда точное число не важно.
Когда брать
- точное число не важно — громкость, яркость, прозрачность: важнее «больше — меньше»;
- диапазон нагляден —
min/maxпоказывают границы, которых в поле ввода не видно; - нужен диапазон «от — до» —
rangeдаёт два бегунка, которые не проходят друг сквозь друга; - у шкалы есть опорные точки —
marksподписывают ступени; - значение применяется по отпусканию —
lazyне шлёт запрос на каждое движение.
Когда взять другое
| Нужно | Берите |
|---|---|
| Нужно точное число | GrNumberInput |
| Оценка звёздами | GrRating |
| Ступеней немного и они названы | GrSegmented / GrRadioGroup |
| Значение показывают, а не задают | GrProgressBar |
Диапазон
<GrSlider v-model="range" range :min="0" :max="100" aria-label="Бюджет" />
Модель — кортеж [lo, hi]; бегунки не перепрыгивают друг друга. Клик по дорожке
уводит ближайший бегунок, а когда оба сошлись в точку — тот, в сторону
которого кликнули: иначе схлопнутый диапазон было бы не развести мышью.
Каждый бегунок получает своё имя: ariaLabel плюс граница из локали
(gr.slider.min / gr.slider.max) — «Бюджет (минимум)».
Ориентация
<GrSlider v-model="volume" orientation="vertical" :style="{ '--gr-slider-length': '12rem' }" />
В вертикальной дорожке минимум внизу. Длина задаётся --gr-slider-length
(по умолчанию 10rem), толщину по-прежнему держит --gr-slider-track-height.
Подсказка уходит вбок — над бегунком она легла бы на дорожку, — а подписи меток
встают справа. aria-orientation следует пропу, клавиатура не меняется: ↑/→
увеличивают, ↓/← уменьшают.
Когда значение уходит наружу
lazy придерживает update:modelValue до конца жеста: во время перетаскивания
бегунок ведёт внутреннее значение, а наружу уходит одно событие на отпускании
(вместе с change). Это для дорогих пересчётов, которым незачем срабатывать на
каждое движение мыши.
Клавиатура коммитит сразу и при lazy: нажатие клавиши дискретно, придерживать
его нечего.
Не доехавший modelValue ставит бегунок на min и объясняется предупреждением
в dev-режиме: иначе смещение и aria-valuenow ушли бы в NaN, а диктор
прочитал бы его вслух.
Метки и подсказка
marks — словарь { [value]: label } или массив значений. Подписи меток
скрыты от скринридера (aria-hidden): значение он читает с самого бегунка, а
подписи стали бы случайным текстом внутри слайдера.
formatTooltip управляет и подсказкой, и aria-valuetext — «$1 200» вместо
голого «1200». Без своего формата aria-valuetext не выставляется: число
самодостаточно.
Оформление
size — xs…lg, читается из GrConfigProvider. Точечная кастомизация — через
переменные: --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 гасит заливку, бегунок и подписи меток токенами --gr-disabled-*, а
не прозрачностью: opacity разбавляет выверенные на AA цвета текста.
Нативная форма
Проп name рендерит input[type="hidden"] со значением; при range — два
инпута с одним именем (lo, hi). Сериализуется модель (после снапа к
step), а не черновик жеста при lazy.
Playground 13
Загружается…
<GrSlider />Установка
npm i @feugene/granularityИмпорт
import { GrSlider } from '@feugene/granularity/components/GrSlider'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Только для чтения: значение видно, но не меняется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | Имя для нативной формы: hidden input на значение, при `range` — два с одним именем. |
max | number | undefined | 100 | — |
orientation | "horizontal" | "vertical" | undefined | "horizontal" | Ориентация дорожки. В вертикальной минимум внизу. |
lazy | boolean | undefined | false | Эмитить `update:modelValue` только по завершении жеста: во время перетаскивания значение ведёт сам слайдер. Клавиатура коммитит сразу — нажатие клавиши дискретно, придерживать его нечего. |
step | number | undefined | 1 | — |
min | number | undefined | 0 | — |
range | boolean | undefined | false | Диапазон с двумя бегунками; модель — кортеж `[lo, hi]`. |
marks | GrSliderMarks | undefined | undefined | Метки делений: `{ [value]: label }` или массив значений. |
showTooltip | boolean | "hover" | "always" | undefined | false | Всплывающее значение над бегунком: `true`/`'hover'` — при hover/drag/focus, `'always'` — всегда. |
formatTooltip | ((value: number) => string) | undefined | undefined | Форматирование значения в tooltip. Оно же уходит в `aria-valuetext`. |
modelValueобязательный | GrSliderModelValue | — | `number` — одиночное значение; `[lo, hi]` — диапазон (при `range=true`). |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: GrSliderModelValue] | — |
change | [value: GrSliderModelValue] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Примеры 4
Одно значение с подсказкой
Базовый ползунок: v-model (число), диапазон min/max, всплывающее значение (show-tooltip + format-tooltip). Полная клавиатура: стрелки меняют на step, PageUp/PageDown — крупный шаг, Home/End — к границам.
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>Каждый бегунок — role="slider" с aria-valuemin/max/now, доступный с клавиатуры и для скринридеров.
Диапазон двумя бегунками
Режим range: модель — кортеж [lo, hi], два бегунка, которые не перепрыгивают друг друга. Клик по дорожке двигает ближайший бегунок.
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>Для диапазона у нижнего бегунка aria-valuemax = значение верхнего, а у верхнего aria-valuemin = значение нижнего — скринридер объявляет корректные границы.
Метки, шаг, размеры и выключенное состояние
Метки делений (marks), фиксированный step, размеры (sm/md/lg) и 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>Свои цвета и размер через переменные CSS
Внешний вид настраивается CSS-переменными на самом слайдере (или любом предке) — без новых пропов: --gr-slider-fill (активная часть), --gr-slider-rail (фон дорожки), --gr-slider-thumb-bg / --gr-slider-thumb-border (заливка и окантовка бегунка), --gr-slider-thumb-size и --gr-slider-track-height (размеры). Незаданные переменные откатываются к дефолтам темы/размера.
<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>Переменные наследуются, поэтому одну тему слайдеров можно задать на контейнере формы, а отдельные слайдеры точечно переопределить.
Доступность
- Паттерн APG
slider- Клавиши
←/↓— шаг вниз,→/↑— шаг вверх,PageUp/PageDown— крупный шаг,Home/End— к краям