Slider 컴포넌트 구현 과정을 공유하고자 한다

Slider 를 구현하면서 Slider 컴포넌트와 hover 시 툴팁 UI 와 나와야 하므로 SliderTooltip 컴포너트를 구현하였다. 한 번에 설명하는 것보다 분리시켜 설멍하는게 좋다고 생각하여 해당 컴포넌트와 스타일링을 같이 설명하고자 한다.
// Slider.tsx
type SliderTooltipProps = {
sliderValue: number;
vertical: boolean;
};
function SliderTooltip({
sliderValue, //
vertical,
children,
}: PropsWithChildren<SliderTooltipProps>) {
return (
<S.SliderTooltip
data-slider-tooltip
className={cns({ vertical })}
style={
{
'--slider-value': `${sliderValue}%`,
} as CSSProperties
}
>
{children}
</S.SliderTooltip>
);
}
--slider-value) 를 전달하였다.export const SliderTooltip = styled.div`
padding: 4px 6px;
color: ${color.gray300};
white-space: nowrap;
background-color: ${color.gray800};
border-radius: 0.25rem;
outline: none;
opacity: 0;
font-size: 11px;
line-height: 1.4;
position: absolute;
left: var(--slider-value);
transform: translateX(0%) translateY(-175%);
&.vertical {
transform: translateY(50%) rotate(90deg);
}
`;
left: var(--slider-value); 을 통해 이동되게 하였다. -webkit-appearance: slider-vertical 속성들을 통해 제어할 수 있다고 판단된다.// Slider.tsx
import { CSSProperties, PropsWithChildren, useState } from 'react';
import * as S from './Slider.styles';
import { theme } from '@/styles/theme.ts';
import cns from 'classnames';
// ... SliderTooltip 구현 내용
export type SliderProps = {
/** 슬라이더의 초기 값 0 ~ 100 */
initialValue?: number;
/** 슬라이더의 너비 */
width?: string;
/** vertical 일 때 slider 높이 */
height?: string;
/** 슬라이더 메인 컬러 */
color?: string;
/** 슬라이더의 라벨 아이템 0 ~ 100 */
items?: number[];
/** */
vertical?: boolean;
};
export function Slider({
initialValue, //
width = '200px',
height = '200px',
color = theme.color.primary,
items = [],
vertical = false,
}: SliderProps) {
const [sliderValue, setSliderValue] = useState(initialValue || 0);
return (
<S.SliderWrapper
className={cns({ vertical })}
style={
{
'--slider-width': width,
'--slider-height': height,
} as CSSProperties
}
>
<S.Slider
type="range" //
value={sliderValue}
color={color}
onChange={(event) => setSliderValue(Number(event.target.value))}
style={
{
'--slider-value': `${sliderValue}%`,
'--slider-color': color,
} as CSSProperties
}
/>
<SliderTooltip sliderValue={sliderValue} vertical={vertical}>
{sliderValue}
</SliderTooltip>
{Array.isArray(items) && (
<S.Labels>
{items?.map((item) => (
<S.Label
value={item} //
color={color}
onClick={() => setSliderValue(Number(item))}
>
{item}%
</S.Label>
))}
</S.Labels>
)}
</S.SliderWrapper>
);
}
// Slider.styles.tsx
export const SliderWrapper = styled.div`
position: relative;
z-index: 0;
user-select: none;
width: var(--slider-width);
height: var(--slider-height);
&.vertical {
transform: rotate(-90deg);
width: var(--slider-height);
}
`;
export const Slider = styled.input`
width: 100%;
-webkit-appearance: none;
position: relative;
background-color: rgba(0, 0, 0, 0.1);
cursor: pointer;
&[type='range'] {
-webkit-appearance: none;
}
&::-webkit-slider-runnable-track {
height: 5px;
background: linear-gradient(
to right,
var(--slider-color) 0%,
var(--slider-color) var(--slider-value),
rgba(0, 0, 0, 0.1) 0%
);
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1.5rem;
height: 1.5rem;
border: 3px solid white;
border-radius: 50%;
background-color: var(--slider-color);
position: absolute;
top: -8px;
left: var(--slider-value);
cursor: pointer;
}
&:hover ~ [data-slider-tooltip],
&:active ~ [data-slider-tooltip] {
opacity: 1;
}
`;
export const Labels = styled.ul`
margin: 0.5rem 0 0;
padding: 0;
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Label = styled.button(({ value, color }) => ({
outline: 'none',
border: 'none',
backgroundColor: 'rgba(0, 0, 0, 0.15)',
color: 'rgba(0, 0, 0, 0.25)',
fontWeight: 600,
padding: '0.3rem 0.7rem',
borderRadius: '10px',
position: 'absolute',
left: `calc(${value}% - 20px)`,
'&:hover': {
backgroundColor: `${color}`,
color: `${theme.color.gray700}`,
},
transform: 'translateY(50%) rotate(90deg)',
}));
[SliderWrapper 스타일]
export const SliderWrapper = styled.div`
position: relative;
z-index: 0;
user-select: none;
width: var(--slider-width);
height: var(--slider-height);
&.vertical {
transform: rotate(-90deg);
width: var(--slider-height);
}
`;
-webkit-appearance: slider-vertical 속성들로 구현하면 좀 더 잘 제어할 수 있을거 같다)[Slider]
export const Slider = styled.input`
width: 100%;
-webkit-appearance: none;
position: relative;
background-color: rgba(0, 0, 0, 0.1);
cursor: pointer;
&[type='range'] {
-webkit-appearance: none;
}
&::-webkit-slider-runnable-track {
height: 5px;
background: linear-gradient(
to right,
var(--slider-color) 0%,
var(--slider-color) var(--slider-value),
rgba(0, 0, 0, 0.1) 0%
);
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1.5rem;
height: 1.5rem;
border: 3px solid white;
border-radius: 50%;
background-color: var(--slider-color);
position: absolute;
top: -8px;
left: var(--slider-value);
cursor: pointer;
}
&:hover ~ [data-slider-tooltip],
&:active ~ [data-slider-tooltip] {
opacity: 1;
}
`;
Slider 를 구현할 때 input 요소의 range 로 구현하는 것이 a11y 에 더 적합하다고 생각하여 input 을 이용하여 구현하였다.
input 의 range 의 기본 스타일이 있어 -webkit-appearance: none; 으로 스타일을 초기화했다.
:-webkit-slider-runnable-track &::-webkit-slider-thumb 속성으로 ragne 스타일 요소를 제어할 수 있어 제어했다.
:-webkit-slider-runnable-track 은 slider-thumb이 움직이는 트랙인 요소를 스타일링한다.to-right 로 오른쪽에서 시작하도록 설정var(--slider-color) 0%, 는 color 색상으로 시작하도록 설정var(--slider-color) var(--slider-value) 은 해당 색상이 --slider-value(ex 50%) 까지 해당 color 로 채우고 이후에는 검은색으로 사라진다.rgba(0, 0, 0, 0.1) 0% 은 검은색으로 사라지는 정도를 표현하였다. (해당 값으로 완전 검은색이 아닌 슬라이더 기본 색상이 보이도록 투명하게 설정했다.&::-webkit-slider-thumb 은 slider-thumb 의 선택기로 슬라이더의 손잡이?의 스타일을 정의할 때 사용할 수 있다.
마지막으로 Slider hover, active 를 한 경우 slider-tooltip 이 보이도록 설정했다.
[SliderLabel]
{Array.isArray(items) && (
<S.Labels>
{items?.map((item) => (
<S.Label
value={item} //
color={color}
onClick={() => setSliderValue(Number(item))}
>
{item}%
</S.Label>
))}
</S.Labels>
)}
export const Labels = styled.ul`
margin: 0.5rem 0 0;
padding: 0;
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Label = styled.button(({ value, color }) => ({
outline: 'none',
border: 'none',
backgroundColor: 'rgba(0, 0, 0, 0.15)',
color: 'rgba(0, 0, 0, 0.25)',
fontWeight: 600,
padding: '0.3rem 0.7rem',
borderRadius: '10px',
position: 'absolute',
left: `calc(${value}% - 20px)`,
'&:hover': {
backgroundColor: `${color}`,
color: `${theme.color.gray700}`,
},
transform: 'translateY(50%) rotate(90deg)',
}));
::-webkit-slider-runnable-track표준이 아니고,현재는 chrome 스타일링만 적용했다. 개선할 점이 너무 많아 보인다!
지금은 바닐라로 구현했는데 괜찮은 라이브러리 도움을 받는 것도 좋은 것 같다. - @reach/slider