Slider
Slider control for Tile UI.
source cached 2026-10-19
packages/react/src/components/slider/slider.tsx
import React, { createContext, useContext, useState } from 'react';
import { clampSliderValue, getSliderOffsetStyle, getSliderPercent, sliderStyleKeys } from '../lib/core';
import type { SliderBaseProps } from '../lib/core';
import styles from './slider.module.scss';
interface SliderContextValue {
value: number;
min: number;
max: number;
step: number;
orientation: 'horizontal' | 'vertical';
disabled: boolean;
setValue: (value: number) => void;
}
const SliderContext = createContext<SliderContextValue | null>(null);
function useSliderContext(): SliderContextValue {
const context = useContext(SliderContext);
if (!context) {
throw new Error('Slider sub-components must be used within <Slider>.');
}
return context;
}
export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'value' | 'defaultValue'>, SliderBaseProps {}
const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
({ className = '', value, defaultValue = 0, min = 0, max = 100, step = 1, orientation = 'horizontal', disabled = false, onValueChange, children, ...props }, ref) => {
const [internalValue, setInternalValue] = useState(defaultValue);
const currentValue = value !== undefined ? value : internalValue;
const clampedValue = clampSliderValue(currentValue, min, max);
function setValue(next: number) {
if (disabled) {
return;
}
const clamped = clampSliderValue(next, min, max);
const stepped = clampSliderValue(min + Math.round((clamped - min) / step) * step, min, max);
if (value === undefined) {
setInternalValue(stepped);
}
onValueChange?.(stepped);
}
function updateFromEvent(event: React.PointerEvent<HTMLDivElement>, element: HTMLElement) {
const rect = element.getBoundingClientRect();
let next: number;
if (orientation === 'vertical') {
next = min + ((event.clientY - rect.top) / rect.height) * (max - min);
} else {
next = min + ((event.clientX - rect.left) / rect.width) * (max - min);
}
setValue(next);
}
function handlePointerDown(event: React.PointerEvent<HTMLDivElement>) {
if (disabled) {
return;
}
event.currentTarget.setPointerCapture(event.pointerId);
updateFromEvent(event, event.currentTarget);
}
function handlePointerMove(event: React.PointerEvent<HTMLDivElement>) {
if (disabled || !event.currentTarget.hasPointerCapture(event.pointerId)) {
return;
}
updateFromEvent(event, event.currentTarget);
}
return (
<SliderContext.Provider value={{ value: clampedValue, min, max, step, orientation, disabled, setValue }}>
<div
{...props}
ref={ref}
data-orientation={orientation}
data-disabled={disabled}
className={`${styles[sliderStyleKeys.root]} ${className}`}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}>
{children}
</div>
</SliderContext.Provider>
);
},
);
Slider.displayName = 'Slider';
export interface SliderTrackProps extends React.HTMLAttributes<HTMLDivElement> {}
const SliderTrack = React.forwardRef<HTMLDivElement, SliderTrackProps>(({ className = '', ...props }, ref) => {
const context = useSliderContext();
return <div ref={ref} data-orientation={context.orientation} className={`${styles[sliderStyleKeys.track]} ${className}`} {...props} />;
});
SliderTrack.displayName = 'SliderTrack';
export interface SliderRangeProps extends React.HTMLAttributes<HTMLDivElement> {}
const SliderRange = React.forwardRef<HTMLDivElement, SliderRangeProps>(({ className = '', ...props }, ref) => {
const context = useSliderContext();
const percent = getSliderPercent(context.value, context.min, context.max);
const style = context.orientation === 'vertical' ? { height: `${percent}%` } : { width: `${percent}%` };
return <div ref={ref} data-orientation={context.orientation} className={`${styles[sliderStyleKeys.range]} ${className}`} style={style} {...props} />;
});
SliderRange.displayName = 'SliderRange';
export interface SliderThumbProps extends React.HTMLAttributes<HTMLSpanElement> {}
const SliderThumb = React.forwardRef<HTMLSpanElement, SliderThumbProps>(({ className = '', onKeyDown, ...props }, ref) => {
const context = useSliderContext();
const offsetStyle = getSliderOffsetStyle(context.value, context.min, context.max, context.orientation);
function handleKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {
onKeyDown?.(event);
if (event.key === 'Home') {
context.setValue(context.min);
event.preventDefault();
return;
}
if (event.key === 'End') {
context.setValue(context.max);
event.preventDefault();
return;
}
const delta =
context.orientation === 'horizontal'
? event.key === 'ArrowRight'
? 1
: event.key === 'ArrowLeft'
? -1
: 0
: event.key === 'ArrowDown'
? 1
: event.key === 'ArrowUp'
? -1
: 0;
if (delta !== 0) {
context.setValue(context.value + delta * context.step);
event.preventDefault();
}
}
return (
<span
{...props}
ref={ref}
role="slider"
tabIndex={context.disabled ? -1 : 0}
aria-disabled={context.disabled}
aria-valuemin={context.min}
aria-valuemax={context.max}
aria-valuenow={context.value}
aria-orientation={context.orientation}
data-orientation={context.orientation}
className={`${styles[sliderStyleKeys.thumb]} ${className}`}
style={offsetStyle}
onKeyDown={handleKeyDown}
/>
);
});
SliderThumb.displayName = 'SliderThumb';
export { Slider, SliderTrack, SliderRange, SliderThumb };
export default Slider;
- Interaction
- carousel
DependenciesBundle weight · light
Registry dependencies
@tile-ui/core@tile-ui/styles