Slider
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
Slider
범위 입력(range). 라벨·현재값·단위·reset 버튼을 한 줄에 정리. CSS 컨트롤 패널이나 설정 화면에서 즐겨 쓰는 입력.
Default (unit + reset)
가장 기본 — label / 현재값 / 단위 / 값이 다르면 reset 버튼 자동 노출.
Source Code
60%
Live Preview
formatValue (소수·커스텀)
formatValue로 표시 형식 커스터마이즈. unit은 무시됨.
Source Code
0.80
Live Preview
Sizes
sm · md 두 단계. 좁은 컨트롤 패널에선 sm 사용.
SM
180°
Live Preview
MD (default)
180°
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Slidertypescript
'use client';
/**
* Slider — 범위 입력 (range).
*
* - label / value(표시) / unit / reset 버튼을 한 줄로 정리
* - formatValue 콜백으로 표시 형식 커스터마이즈 (지정 시 unit 무시)
* - resetValue 지정 시 현재값이 다르면 reset 버튼 노출
* - size: sm / md
* - 트랙 채움은 linear-gradient(인라인 style)로 % 표시
*/
import { forwardRef, useId, type InputHTMLAttributes, type ReactNode } from 'react';
import { cn } from '@/lib/cn';
export type SliderSize = 'sm' | 'md';
export interface SliderProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange' | 'type' | 'value'> {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
size?: SliderSize;
label?: ReactNode;
description?: ReactNode;
/** 값 옆에 표시할 단위 (px, %, deg 등). formatValue가 있으면 무시. */
unit?: string;
/** 값 표시 형식 커스터마이즈. */
formatValue?: (value: number) => string;
/** 지정 시, 현재값이 다르면 우상단에 reset 버튼 노출 */
resetValue?: number;
}
export const Slider = forwardRef<HTMLInputElement, SliderProps>(
(
{
value,
onChange,
min = 0,
max = 100,
step = 1,
size = 'md',
label,
description,
unit,
formatValue,
resetValue,
disabled,
className,
id,
...rest
},
ref,
) => {
const reactId = useId();
const inputId = id ?? `slider-${reactId}`;
const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
const display = formatValue ? formatValue(value) : `${value}${unit ?? ''}`;
const showReset = resetValue !== undefined && value !== resetValue;
return (
<div className={cn('w-full', disabled && 'opacity-60', className)}>
{(label || resetValue !== undefined) && (
<div className="mb-1.5 flex items-center justify-between gap-2">
{label && (
<label
htmlFor={inputId}
className="text-[11px] font-bold uppercase tracking-[0.06em] text-slate-700"
>
{label}
</label>
)}
<div className="flex items-center gap-2">
<span className="tabular-nums text-xs font-bold text-slate-900">
{display}
</span>
{showReset && (
<button
type="button"
onClick={() => onChange(resetValue as number)}
className="text-[10px] font-bold uppercase tracking-wider text-slate-400 transition-colors hover:text-violet-600"
aria-label="기본값으로 초기화"
>
reset
</button>
)}
</div>
</div>
)}
<input
ref={ref}
id={inputId}
type="range"
value={value}
min={min}
max={max}
step={step}
disabled={disabled}
onChange={(e) => onChange(Number(e.target.value))}
style={{
background: `linear-gradient(to right, #7c3aed 0%, #7c3aed ${percent}%, #e2e8f0 ${percent}%, #e2e8f0 100%)`,
}}
className={cn(
'w-full cursor-pointer appearance-none rounded-full outline-none transition-all',
size === 'sm' ? 'h-1' : 'h-1.5',
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:ring-2 [&::-webkit-slider-thumb]:ring-violet-500 [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-110',
'[&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-violet-500 [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-md [&::-moz-range-thumb]:transition-transform hover:[&::-moz-range-thumb]:scale-110',
'focus-visible:ring-2 focus-visible:ring-violet-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white',
disabled && 'cursor-not-allowed',
)}
{...rest}
/>
{description && (
<p className="mt-1 text-[11px] text-slate-500">{description}</p>
)}
</div>
);
},
);
Slider.displayName = 'Slider';