import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
<Slider defaultValue={40} style={{ width: '100%' }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Volume' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>- Поддержка браузеров
- Широко доступно
- Chrome 111
- Edge 111
- Firefox 121
- Safari 15.4
- Safari на iOS 15.4
- Размер в бандле
- Протестирован в
- 68 тестов · 6 нояб.
- :has()
- Oklab and OkLCh — приходит из дизайн-токенов
- clip-path
- Logical properties
- :dir() — от неё зависят только макеты справа налево
- Vertical form controls — от неё зависит только вертикальная ориентация
- :focus-visible
- :where()
Кратко
Slider — составной компонент для выбора одного или нескольких чисел из ограниченного диапазона. Каждый SliderThumbInput — это нативный <input type="range">, наложенный на один SliderTrack, поэтому клавиатура, фокус, отправка и сброс формы и вспомогательные технологии работают как с нативным элементом. Два бегунка превращают слайдер в слайдер диапазона.
Импорт
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbInput,
SliderThumbIndicator,
SliderTrack,
useSliderContext,
useSliderThumbContext,
type SliderOrientation,
type SliderProps,
type SliderRangeProps,
type SliderSize,
type SliderThumbProps,
type SliderThumbInputProps,
type SliderThumbIndicatorProps,
type SliderTrackProps,
type SliderVariant,
} from '@vega-ui/react';Экспортируемые типы
SliderPropsSliderTrackPropsSliderRangePropsSliderThumbPropsSliderThumbInputPropsSliderThumbIndicatorPropsSliderSizeSliderVariantSliderOrientation
Минимальная композиция
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
export default function MinimalComposition() {
return (
<Slider defaultValue={50} style={{ width: '100%', maxWidth: 400 }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Zoom' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
);
}Обязательные части
Slider: корень, значения всех бегунков и общие настройки диапазонаSliderThumb: один бегунок на каждое значениеSliderThumbInput: нативное поле range у бегунка
Необязательные части
SliderTrack: видимая дорожкаSliderRange: заполненная часть дорожки внутриSliderTrackSliderThumbIndicator: визуальный бегунок; без него бегунок невидим
Порядок композиции
SliderSliderTrack>SliderRangeSliderThumb— по одному на значение,indexпо порядку отminкmax, внутриSliderThumbInputиSliderThumbIndicator
Варианты
- Размер:
sm,md,lg - Вариант:
primary,secondary - Ориентация:
horizontalилиvertical - Бегунки: одно значение или диапазон из нескольких
- Модель значения: контролируемая или неконтролируемая на корне, число для одного бегунка или массив для нескольких
- Шаг: числовой шаг или
'any'
Пограничные случаи
- Попасть в точное значение слайдером труднее, чем набрать его в поле.
- Вертикальным слайдерам нужна явная высота.
- Бегунки не могут обогнать друг друга;
minStepsBetweenThumbsсохраняет промежуток между ними. - Без
valueилиdefaultValueу слайдера один бегунок посередине, как у<input type="range">.
Частые ошибки
- Слайдер используют там, где главное — точный ввод числа.
- Забывают показать видимое значение в сценариях, где важна точность.
- Оставляют бегунки без
aria-labelилиaria-labelledby. - Рендерят число
SliderThumb, отличное от числа значений, или дают двум бегункам одинаковыйindex.
Примеры
Базовый
Базовый: слайдер с подписью и значением по умолчанию
Неконтролируемый слайдер: defaultValue задаёт начальное значение, а name на корне доходит до поля бегунка. SliderThumbInput — нативный <input type='range'>, поэтому видимый Label ссылается на его id.
import {
Label,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
export default function SliderWithDefaultValue() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 400 }}>
<Label htmlFor='notification-volume'>Notification volume</Label>
<Slider defaultValue={30} name='notification-volume'>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput id='notification-volume' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</div>
);
}Базовый: два бегунка для диапазона
Массив в defaultValue задаёт по значению на каждый SliderThumb по его index. Каждое поле бегунка получает свой name и aria-label, а minStepsBetweenThumbs не даёт смене стать короче часа.
import {
Fieldset,
FieldsetLegend,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
export default function RangeSlider() {
return (
<Fieldset style={{ width: '100%', maxWidth: 400 }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Working hours</FieldsetLegend>
<Slider min={0} max={24} defaultValue={[9, 18]} minStepsBetweenThumbs={1}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb index={0}>
<SliderThumbInput name='start-hour' aria-label='Start time' />
<SliderThumbIndicator />
</SliderThumb>
<SliderThumb index={1}>
<SliderThumbInput name='end-hour' aria-label='End time' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: значение, управляющее превью
value с onChangeValue держит число в родителе: подпись и образец цвета следуют за ним во время перетаскивания. aria-valuetext объявляет значение вместе с единицей измерения.
import { useState } from 'react';
import {
Label,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
export default function ControlledBrightness() {
const [brightness, setBrightness] = useState(60);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 400 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 'var(--spacing-6)' }}>
<Label htmlFor='screen-brightness'>Screen brightness</Label>
<Text size={3} aria-hidden style={{ fontVariantNumeric: 'tabular-nums' }}>{brightness}%</Text>
</div>
<Slider value={brightness} onChangeValue={setBrightness}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput id='screen-brightness' aria-valuetext={`${brightness}%`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</div>
<div
aria-hidden
style={{
height: 64,
borderRadius: 'var(--radius-2)',
background: 'var(--color-primary-500)',
filter: `brightness(${0.2 + brightness / 100})`,
}}
/>
</div>
);
}Контролируемый/с состоянием: фильтр цены по списку товаров
Контролируемый диапазон с двумя бегунками фильтрует список прямо во время движения. Цены форматируются одним Intl.NumberFormat, который используется и для подписи, и для aria-valuetext каждого бегунка.
import { useState } from 'react';
import {
Fieldset,
FieldsetLegend,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
const products = [
{ name: 'Trail runner', price: 89 },
{ name: 'Rain shell', price: 149 },
{ name: 'Down jacket', price: 279 },
{ name: 'Wool beanie', price: 25 },
{ name: 'Hiking boots', price: 189 },
{ name: 'Day pack', price: 65 },
{ name: 'Fleece vest', price: 79 },
{ name: 'Expedition parka', price: 540 },
];
const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
export default function ControlledRange() {
const [price, setPrice] = useState([50, 200]);
const matching = products.filter((product) => product.price >= price[0] && product.price <= price[1]);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 400 }}>
<Fieldset>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Price</FieldsetLegend>
<Text size={3} aria-hidden style={{ fontVariantNumeric: 'tabular-nums' }}>{currency.format(price[0])} – {currency.format(price[1])}</Text>
<Slider min={0} max={600} step={10} minStepsBetweenThumbs={5} value={price} onChangeValue={setPrice}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb index={0}>
<SliderThumbInput aria-label='Minimum price' aria-valuetext={currency.format(price[0])} />
<SliderThumbIndicator />
</SliderThumb>
<SliderThumb index={1}>
<SliderThumbInput aria-label='Maximum price' aria-valuetext={currency.format(price[1])} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
<Text size={2} aria-live='polite' style={{ color: 'var(--label-secondary)' }}>
{matching.length} of {products.length} products{matching.length > 0 ? `: ${matching.map((product) => product.name).join(', ')}` : ''}
</Text>
</div>
);
}Контролируемый/с состоянием: добавление и удаление бегунков
Число бегунков следует за длиной массива value: рендерите по одному SliderThumb на элемент. Бегунки остаются упорядоченными и на расстоянии minStepsBetweenThumbs, а каждый получает подпись по своей позиции.
import { useState } from 'react';
import {
Button,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
const tierNames = ['Free', 'Starter', 'Growth', 'Scale', 'Enterprise'];
export default function AnyNumberOfThumbs() {
const [limits, setLimits] = useState([1000, 10000, 50000]);
const addLimit = () => {
const last = limits[limits.length - 1];
if (last >= 95000) return;
setLimits([...limits, Math.min(last + 20000, 100000)]);
};
const removeLimit = () => {
setLimits(limits.slice(0, -1));
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 400 }}>
<Slider min={0} max={100000} step={1000} minStepsBetweenThumbs={5} value={limits} onChangeValue={setLimits}>
<SliderTrack>
<SliderRange />
</SliderTrack>
{limits.map((limit, index) => (
<SliderThumb key={index} index={index}>
<SliderThumbInput aria-label={`${tierNames[index]} tier limit`} aria-valuetext={`${limit.toLocaleString('en-US')} requests`} />
<SliderThumbIndicator />
</SliderThumb>
))}
</Slider>
<Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>
{limits.map((limit, index) => `${tierNames[index]}: up to ${limit.toLocaleString('en-US')}`).join(' · ')}
</Text>
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button size='sm' variant='secondary' appearance='ghost' onClick={addLimit} disabled={limits.length === tierNames.length}>Add tier</Button>
<Button size='sm' variant='secondary' appearance='ghost' onClick={removeLimit} disabled={limits.length === 2}>Remove last tier</Button>
</div>
</div>
);
}Форма/интеграция
Форма/интеграция: чтение значений через FormData
Диапазон с name на корне отправляет по записи на каждый бегунок, поэтому FormData.getAll('age') возвращает обе границы; одиночный бегунок читается через FormData.get. Сброс формы возвращает defaultValue.
import { FormEvent, useState } from 'react';
import {
Button,
Fieldset,
FieldsetLegend,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
export default function NativeFormValue() {
const [result, setResult] = useState('');
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setResult(`age=${data.getAll('age').join('–')}, radius=${data.get('radius')} km`);
};
return (
<form onSubmit={handleSubmit} onReset={() => setResult('')} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 400 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<Fieldset>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Audience age</FieldsetLegend>
<Slider min={18} max={65} defaultValue={[25, 40]} name='age'>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb index={0}>
<SliderThumbInput aria-label='Minimum age' />
<SliderThumbIndicator />
</SliderThumb>
<SliderThumb index={1}>
<SliderThumbInput aria-label='Maximum age' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
<Fieldset>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Delivery radius</FieldsetLegend>
<Slider min={1} max={50} defaultValue={10}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput name='radius' aria-label='Radius in kilometers' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-6)' }}>
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Save campaign</Button>
<Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
</div>
<Text size={2} aria-live='polite' style={{ color: 'var(--label-secondary)' }}>{result}</Text>
</div>
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: вертикальный слайдер
orientation='vertical' растёт снизу вверх, и ему нужна явная высота. Значение показано под дорожкой, а клавиатура работает по обеим осям стрелок.
import { useState } from 'react';
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
export default function VerticalSlider() {
const [level, setLevel] = useState(70);
return (
<div style={{ display: 'flex', alignItems: 'stretch', gap: 'var(--spacing-12)' }}>
<div style={{ display: 'grid', justifyItems: 'center', gap: 'var(--spacing-4)' }}>
<Slider orientation='vertical' value={level} onChangeValue={setLevel} style={{ height: 200 }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Master volume' aria-valuetext={`${level}%`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
<Text size={1} fontWeight={600} style={{ fontVariantNumeric: 'tabular-nums' }}>{level}%</Text>
</div>
<Text size={2} style={{ maxWidth: 220, color: 'var(--label-secondary)' }}>
A vertical slider needs an explicit height. Arrow Up and Arrow Right raise the value, Page Up and Page Down jump by a tenth.
</Text>
</div>
);
}Раскладка/оверлей: слайдер громкости в поповере
Триггер-иконка открывает Popover с вертикальным слайдером. aria-label триггера включает текущее значение, поэтому оно объявляется без открытия поповера, а иконка следует за значением.
import { useState } from 'react';
import {
Icon,
IconButton,
Popover,
PopoverContent,
PopoverTrigger,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
import { Volume1Icon, Volume2Icon, VolumeXIcon } from '@vega-ui/icons';
export default function SliderInAPopover() {
const [volume, setVolume] = useState(40);
let VolumeIcon = Volume2Icon;
if (volume === 0) {
VolumeIcon = VolumeXIcon;
} else if (volume < 50) {
VolumeIcon = Volume1Icon;
}
return (
<Popover role='dialog' placement='top'>
<PopoverTrigger asChild>
<IconButton aria-label={`Volume, ${volume}%`} variant='secondary' appearance='ghost'>
<Icon size='sm'><VolumeIcon /></Icon>
</IconButton>
</PopoverTrigger>
<PopoverContent aria-label='Volume'>
<div style={{ padding: 'var(--spacing-6)' }}>
<Slider orientation='vertical' value={volume} onChangeValue={setVolume} style={{ height: 140 }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Volume' aria-valuetext={`${volume}%`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</div>
</PopoverContent>
</Popover>
);
}Ошибка
Ошибка: значение выше лимита политики
Слайдер допускает весь диапазон, но значения выше лимита политики невалидны: поле бегунка получает aria-invalid и aria-describedby со ссылкой на ошибку, которая исчезает, когда значение возвращается в пределы лимита.
import { useState } from 'react';
import {
HelperText,
Label,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
const policyLimit = 80;
export default function ValueOutsidePolicyHint() {
const [cpu, setCpu] = useState(90);
const invalid = cpu > policyLimit;
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 400 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 'var(--spacing-6)' }}>
<Label htmlFor='cpu-limit'>CPU limit</Label>
<Text size={3} aria-hidden style={{ fontVariantNumeric: 'tabular-nums' }}>{cpu}%</Text>
</div>
<Slider name='cpu-limit' value={cpu} onChangeValue={setCpu}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput
id='cpu-limit'
aria-valuetext={`${cpu}%`}
aria-invalid={invalid}
aria-describedby={invalid ? 'cpu-limit-error' : undefined}
/>
<SliderThumbIndicator />
</SliderThumb>
</Slider>
{invalid && (
<HelperText id='cpu-limit-error' error>
Shared clusters allow up to {policyLimit}%. Lower the limit to save.
</HelperText>
)}
</div>
);
}Отключённый
Отключённый: заблокированное значение и отключение через fieldset
disabled на корне блокирует все бегунки; причина связана через aria-describedby. Слайдер внутри Fieldset disabled отключается нативно и тоже выглядит отключённым — здесь, пока включена автоматическая яркость.
import { useState } from 'react';
import {
Fieldset,
FieldsetLegend,
HelperText,
Label,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Switch,
SwitchIndicator,
SwitchInput,
} from '@vega-ui/react';
export default function LockedSlider() {
const [automatic, setAutomatic] = useState(true);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 400 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='retention-days'>Log retention</Label>
<Slider min={7} max={365} defaultValue={90} disabled>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput id='retention-days' aria-valuetext='90 days' aria-describedby='retention-note' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
<HelperText id='retention-note'>Set by your compliance policy: 90 days.</HelperText>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Label htmlFor='auto-brightness'>Adjust brightness automatically</Label>
<Switch>
<SwitchInput id='auto-brightness' checked={automatic} onChange={(event) => setAutomatic(event.currentTarget.checked)} />
<SwitchIndicator />
</Switch>
</div>
<Fieldset disabled={automatic}>
<FieldsetLegend size={3} style={{ marginBlockEnd: 'var(--spacing-3)' }}>Manual brightness</FieldsetLegend>
<Slider defaultValue={60}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Brightness' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
</div>
</div>
);
}Пограничный случай
Пограничный случай: произвольная точность со step any
step='any' убирает сетку шагов, и значение может быть любой дробью между min и max. Стрелки тогда сдвигают на сотую часть диапазона; подпись округляет значение для показа.
import { useState } from 'react';
import {
Label,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
export default function FreePrecisionStep() {
const [opacity, setOpacity] = useState(0.35);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 400 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 'var(--spacing-6)' }}>
<Label htmlFor='overlay-opacity'>Overlay opacity</Label>
<Text size={3} aria-hidden style={{ fontVariantNumeric: 'tabular-nums' }}>{opacity.toFixed(3)}</Text>
</div>
<Slider min={0} max={1} step='any' name='overlay-opacity' value={opacity} onChangeValue={setOpacity}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput id='overlay-opacity' aria-valuetext={`${Math.round(opacity * 100)}%`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</div>
);
}Пограничный случай: текст значения с единицами и отрицательным минимумом
Диапазон от −10 до 30 с aria-valuetext, чтобы скринридер говорил «20 °C», а не голое число.
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
import { useState } from 'react';
export default function ValueTextForScreenReaders() {
const [temperature, setTemperature] = useState(20);
return (
<Slider min={-10} max={30} value={temperature} onChangeValue={setTemperature} style={{ width: '100%', maxWidth: 400 }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label='Temperature' aria-valuetext={`${temperature} °C`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
);
}Пограничный случай: прилипание к меткам datalist
<datalist>, подключённый через list, заставляет нативное поле прилипать к своим значениям при перетаскивании. Браузер не рисует метки, поэтому, если они должны быть видны, нарисуйте их сами.
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
export default function MarkersFromADatalist() {
return (
<>
<datalist id='zoom-markers'>
<option value='0' />
<option value='50' />
<option value='100' />
</datalist>
<Slider defaultValue={40} style={{ width: '100%', maxWidth: 400 }}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput list='zoom-markers' aria-label='Zoom' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</>
);
}Пограничный случай: группа вертикальных слайдеров с отрицательным диапазоном
Пять вертикальных слайдеров от −12 до +12 дБ образуют эквалайзер внутри подписанной role='group'. Каждый бегунок назван по своей полосе и объявляет усиление в дБ.
import {
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const bands = ['60 Hz', '230 Hz', '910 Hz', '3.6 kHz', '14 kHz'];
export default function VerticalGroupNegativeRange() {
const [gains, setGains] = useState([4, 2, 0, -2, 3]);
const changeGain = (band: number, gain: number) => {
setGains((current) => current.map((value, index) => (index === band ? gain : value)));
};
return (
<div role='group' aria-label='Equalizer' style={{ display: 'flex', gap: 'var(--spacing-16)' }}>
{bands.map((band, index) => (
<div key={band} style={{ display: 'grid', justifyItems: 'center', gap: 'var(--spacing-4)' }}>
<Slider
orientation='vertical'
min={-12}
max={12}
value={gains[index]}
onChangeValue={(gain) => changeGain(index, gain)}
style={{ height: 200 }}
>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb>
<SliderThumbInput aria-label={band} aria-valuetext={`${gains[index]} dB`} />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
<Text size={1} fontWeight={600}>{band}</Text>
</div>
))}
</div>
);
}Анатомия
Обзор
Slider хранит значения всех бегунков и общие настройки (min, max, step, orientation, disabled, name). Каждый SliderThumb объединяет SliderThumbInput — нативный <input type="range">, наложенный на свой участок дорожки между соседями, — и SliderThumbIndicator, который рисуется в точке его значения.
Обязательные части
Slider
Обязательна. Передаёт бегункам настройки диапазона и не даёт соседним бегункам сблизиться. Вертикальному слайдеру нужна высота от родителя.
SliderThumb
Обязательна. Один бегунок: index выбирает его элемент в value корня, а переменные положения задаются для его частей.
SliderThumbInput
Обязательна, прямой потомок SliderThumb. Контролируемое нативное поле range: фокус, клавиатура, значение в форме и доступность обеспечивает браузер; его min и max — это соседи.
Необязательные части
SliderThumbIndicator
Визуальный бегунок, прямой потомок SliderThumb. Может содержать что угодно: пузырёк со значением, иконку, изображение. Пропускает указатель к нативному полю под ним.
SliderTrack
Видимая дорожка. Без неё бегунки всё равно работают над невидимой дорожкой.
SliderRange
Заполненная часть дорожки: от min до единственного бегунка или между первым и последним бегунками.
Порядок композиции
SliderSliderTrack>SliderRangeSliderThumb— по одному на значениеSliderThumbInputSliderThumbIndicator
Корректные варианты композиции
import {
Fieldset,
FieldsetLegend,
Slider,
SliderRange,
SliderThumb,
SliderThumbIndicator,
SliderThumbInput,
SliderTrack,
} from '@vega-ui/react';
export default function RangeSlider() {
return (
<Fieldset style={{ width: '100%', maxWidth: 400 }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Working hours</FieldsetLegend>
<Slider min={0} max={24} defaultValue={[9, 18]} minStepsBetweenThumbs={1}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb index={0}>
<SliderThumbInput name='start-hour' aria-label='Start time' />
<SliderThumbIndicator />
</SliderThumb>
<SliderThumb index={1}>
<SliderThumbInput name='end-hour' aria-label='End time' />
<SliderThumbIndicator />
</SliderThumb>
</Slider>
</Fieldset>
);
}Некорректные варианты композиции
Корень без бегунков
Дорожка рендерится, но взаимодействовать и отправлять нечего.
Бегунки вне корня
SliderThumb читает настройки из корня и позиционируется над дорожкой корня.
Поле ввода или индикатор вне SliderThumb
Они читают свой index из SliderThumb; стили наведения, нажатия, фокуса и отключённого состояния индикатора следуют за полем ввода того же SliderThumb.
Вертикальный слайдер без высоты
Элемент рендерится, но область взаимодействия схлопывается.