1. 들어가기 전에…
범위 슬라이더(range slider)는 사용자가 특정 범위 내에서 최소, 최대값을 선택할 수 있는 UI 요소이다.
이번 포스팅에서는 두 개의 input
을 이용해 범위 슬라이더를 구현하는 방법에 대해 알아보았다!
이 포스팅을 다 읽고 나면 아래와 같은 형태의 범위 슬라이드를 구현할 수 있다 🙂
2. 어떻게 구현할 수 있을까?
1) HTML 구조
💡 먼저, 범위 슬라이더를 구성할 기본 HTML 구조를 정의해야한다.
구조는 두 개의input
요소와 진행 상태를 표시하는 요소, 두 개의 핸들로 구성된다.
(1) 코드보기
<div class="range-slider-container">
<div class="slider-track">
<div class="progress"></div>
</div>
<label>
<span class="handle min"></span>
<input type="range" class="min-range range-input">
</label>
<label>
<span class="handle max"></span>
<input type="range" class="max-range range-input">
</label>
</div>
요소 | 역할 |
---|---|
range-slider-container |
- 최상위 컨테이너 |
slider-track |
- 슬라이더의 트랙 또는 배경 역할 |
progress |
- 슬라이더에서 선택된 범위를 시각적으로 나타내는 요소 - min-range 및 max-range 입력 값에 따라 너비나 위치가 변경됨 |
handle min |
- 슬라이더의 최소값을 설정하는 핸들(손잡이) - 사용자가 드래그하여 범위의 최솟값 설정 가능함 - min-range input 요소와 연결됨 |
handle max |
- 슬라이더의 최대 값을 설정하는 핸들(손잡이) - 사용자가 드래그하여 범위의 최댓값을 설정 가능함 - max-range 입력 요소와 연결됨 |
min-range |
- 범위의 최솟값을 설정하는 range input 요소 |
max-range |
- 범위의 최댓값을 설정하는 range input 요소 |
2) CSS 스타일링
💡 CSS의 경우, CSS 변수로 값의 최소, 최대, 그리고 슬라이더의 스타일을 선언한다.
이렇게 선언된 CSS변수는 JS에서 슬라이더 값을 계산할 때 쓰인다.
(1) CSS 변수 선언
:root {
/* 범위 값 */
--min-value: 10;
--max-value: 200;
--range-step: 5;
}
min-value
와max-value
는 슬라이더의 최소 및 최대 값이다.range-step
은 슬라이드 한 스탭당 움직일 값이다.
:root {
/* 슬라이더 및 진행 막대 스타일 */
--range-height: 8px;
--slider-width: 350px;
--slider-bg: #e7eaf6;
--progress-color: #a2a8d3;
}
range-height
는 슬라이더의 높이값이다. 자식요소의 높이도 동일하게 지정된다.slider-width
는 슬라이더의 너비로, 자식요소의 너비도 동일하게 지정된다.slider-bg
는 슬라이더의 배경색이며,progress-color
는 (선택한 범위를 나타내는)진행 막대의 색상이다.
:root {
/* 핸들 스타일 */
--handle-size: 20px;
--handle-color: #38598b;
/* 핸들 그림자 스타일 */
--handle-effect-size: 5px;
--handle-hover-color: rgba(17, 63, 103, 0.29);
--handle-active-color: rgba(17, 63, 103, 0.49);
}
handle-size
는 슬라이드 핸들의 크기이며,handle-color
는 핸들의 색상이다.handle-effect-size
는 핸들에 마우스 호버했을 때 보여줄 핸들 그림자 크기이다. 핸들 그림자의 경우, 스타일링을 위해 넣은 요소라 없어도 무방하다.handle-hover-color
는 핸들을 호버했을 때 그림자 색상이며,handle-active-color
는 핸들이 활성화됐을 때 그림자 색상이다.
(2) 슬라이더 트랙 스타일링하기
.range-slider-container {
position: relative;
.slider-track {
position: relative;
width: var(--slider-width);
height: var(--range-height);
border-radius: 4px;
background-color: var(--slider-bg);
}
}
.slider-track
은 범위 슬라이더의 트랙 역할을 한다.- css변수(
slider-width
,range-height
)로 트랙의 너비와 높이를 지정한다. - 그리고 css 변수(
slider-bg
)로 트랙의 배경색을 지정해준다.
(3) 진행 막대 스타일링하기
.slider-track .progress {
height: 100%;
position: absolute;
left: 0;
right: 0;
border-radius: 5px;
background-color: var(--progress-color);
}
.progress
는 범위 슬라이더에서 현재 선택된 범위를 나타낸다.- 진행 막대의 높이는 100%로 지정해, 슬라이더 트랙(
.slider-track
) 높이와 동일하게 설정한다. - 그리고 현재 선택된 범위에 따라 요소의 위치가 변해야하므로
position
은absolute
로 지정해야한다. 그러면 기존slider-track
요소와 겹치게 된다. left
와right
을0
으로 지정해, 진행막대의 초기 위치를 슬라이더의 왼쪽과 오른쪽 끝으로 맞춘다.var(--progress-color)
로 진행 막대의 색상을 설정한다.
(4) 슬라이더 입력을 담당하는 input 스타일링하기
.range-slider-container .range-input {
margin: 0;
top: 0;
left: 0;
position: absolute;
width: 100%;
height: var(--range-height);
/** input range의 기본 스타일 제거 */
background: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
/** input range을 컨트롤할 수 없게 */
pointer-events: none;
}
- RangeSlider의 구현 핵심은
type=range
인input
2개를 겹쳐서 사용하는 것이다. - input의 위치를
absolute
로 지정해, 슬라이드 트랙에 겹칠 수 있게 배치한다. - 우리는 지정된 핸들로만 범위를 조정하길 원하므로, input range에 포인트로 상호작용하기 못하도록
pointer-events: none
를 지정한다.
.range-slider-container .range-input {
&::-webkit-slider-thumb {
height: var(--handle-size);
width: var(--handle-size);
border-radius: 50%;
background: transparent; /* 배경색을 투명으로 지정해, 시각적으로 숨기기! */
pointer-events: auto;
-webkit-appearance: none;
cursor: pointer;
}
&::-moz-range-thumb {
height: var(--handle-size);
width: var(--handle-size);
border: none;
border-radius: 50%;
background: transparent; /* 배경색을 투명으로 지정해, 시각적으로 숨기기! */
pointer-events: auto;
-moz-appearance: none;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.05);
}
}
- 기존 range input의 핸들(
slider-thumb
)대신 커스텀 핸들(.handle
)을 쓰기 위해, 기존 range input의 핸들을 시각적으로 숨긴다.
(5) 슬라이더 핸들 스타일링하기
.range-slider-container .handle {
width: var(--handle-size);
height: var(--handle-size);
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
background-color: var(--handle-color);
}
- 직접 조작할 커스텀 핸들을 스타일링한다.
- CSS 변수(
handle-size
)로 핸들의 너비 높이를 지정한다. - 현재 슬라이드 최소, 최대값 위치에 핸들이 위치해야하므로
position
은absolute
로 지정한다. - 핸들은 슬라이드 트랙 기준, 수직 중앙 위치하기 위해
transform: translate(-50%, -50%)
를 지정한다. - CSS 변수(
handle-color
)로 핸들의 색상을 지정한다.
3) JavaScript 코드
💡
RangeSlider
클래스는 HTML 슬라이더의 동작을 제어한다.
이 클래스는 CSS 변수값으로 슬라이더의 최소, 최대 값 및 스텝 크기를 설정하고, 슬라이더 값을 업데이트한다.
- 전체코드는 아래와 같으며, 이제 각 코드별 역할을 설명하겠다.
class RangeSlider {
// (1) 초기화하기
constructor() {
this.constants = {
MAX_VALUE: this.getGlobalCssValue('--max-value'),
MIN_VALUE: this.getGlobalCssValue('--min-value'),
RANGE_STEP: this.getGlobalCssValue('--range-step'),
HANDLE_SIZE: this.getGlobalCssValue('--handle-size'),
get RANGE() {
return this.MAX_VALUE - this.MIN_VALUE;
}
};
this.elements = {
progress: document.querySelector('.progress'),
minRange: document.querySelector('.min-range'),
maxRange: document.querySelector('.max-range'),
handles: document.querySelectorAll('.handle')
};
this.elements.minRange.addEventListener("input", (e) => {
this.setStartValue(+e.target.value);
});
this.elements.maxRange.addEventListener("input", (e) => {
this.setEndValue(+e.target.value);
});
}
// (2) CSS 변수값 가져오기
getGlobalCssValue(key) {
const property = getComputedStyle(document.documentElement).getPropertyValue(key);
return property ? parseFloat(property) : 0;
}
// (3) 초기값 설정하기
init({ min, max }) {
const { MIN_VALUE, MAX_VALUE, RANGE_STEP } = this.constants;
const { minRange, maxRange } = this.elements;
minRange.min = maxRange.min = MIN_VALUE;
minRange.max = maxRange.max = MAX_VALUE;
minRange.step = maxRange.step = RANGE_STEP;
minRange.value = min;
maxRange.value = max;
this.setStartValue(min);
this.setEndValue(max);
}
// (4) 핸들 위치 계산하기
setHandlePos(range, handle) {
const { MIN_VALUE, RANGE, HANDLE_SIZE } = this.constants;
const percentage = (range.value - MIN_VALUE) / RANGE;
const offset = HANDLE_SIZE / 2 - HANDLE_SIZE * percentage;
const left = `calc(${percentage * 100}% + ${offset}px)`;
handle.style.left = left;
}
// (5) 최솟값 계산하기
setStartValue(v) {
const { minRange, maxRange, progress, handles } = this.elements;
if (v >= +maxRange.value) {
v = +maxRange.value - this.constants.RANGE_STEP;
minRange.value = v;
}
const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
progress.style.left = `${(value / this.constants.RANGE) * 100}%`;
this.setHandlePos(minRange, handles[0]);
}
// (6) 최댓값 계산하기
setEndValue(v) {
const { minRange, maxRange, progress, handles } = this.elements;
if (v <= +minRange.value) {
v = +minRange.value + this.constants.RANGE_STEP;
maxRange.value = v;
}
const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
progress.style.right = `${100 - (value / this.constants.RANGE) * 100}%`;
this.setHandlePos(maxRange, handles[1]);
}
// (5) 최솟값 계산하기
getCurrStep(v) {
return (v - this.constants.MIN_VALUE) / this.constants.RANGE_STEP;
}
}
// 사용 예시
const slider = new RangeSlider();
slider.init({ min: 10, max: 150 });
(1) constructor, 값 초기화하기
constructor() {
// (a)
this.constants = {
MAX_VALUE: this.getGlobalCssValue('--max-value'),
MIN_VALUE: this.getGlobalCssValue('--min-value'),
RANGE_STEP: this.getGlobalCssValue('--range-step'),
HANDLE_SIZE: this.getGlobalCssValue('--handle-size'),
get RANGE() {
return this.MAX_VALUE - this.MIN_VALUE;
}
};
// (b)
this.elements = {
progress: document.querySelector('.progress'),
minRange: document.querySelector('.min-range'),
maxRange: document.querySelector('.max-range'),
handles: document.querySelectorAll('.handle')
};
// (c)
this.elements.minRange.addEventListener("input", (e) => {
this.setStartValue(+e.target.value);
});
this.elements.maxRange.addEventListener("input", (e) => {
this.setEndValue(+e.target.value);
});
}
constructor
은 RangeSlider 클래스에서 사용할 변수값을 선언하고 이벤트를 등록한다.
번호 | 코드 설명 |
---|---|
(a) 상수 초기화 | - CSS 변수에서 값을 가져와 constants 객체에 저장한다. ex) 슬라이더의 최대값, 최소값, 단계, 핸들 크기 등 |
(b) DOM 요소 초기화 | - 슬라이더와 관련된 DOM 요소를 elements 객체에 저장한다. |
(c) 이벤트 리스너 등록 | - minRange와 maxRange 요소에 input 이벤트 리스너를 등록한다. - minRange와 maxRange 요소의 값이 변경될 때 setStartValue와 setEndValue를 호출해 범위값을 업데이트한다. |
(2) getGlobalCssValue, CSS 변수 값 가져오는 함수
getGlobalCssValue(key) {
// (a)
const property = getComputedStyle(document.documentElement).getPropertyValue(key);
// (b)
return property ? parseFloat(property) : 0;
}
getGlobalCssValue
함수는 CSS에서 선언한 CSS 변수값에 접근한다.
번호 | 코드 설명 |
---|---|
(a) CSS 변수 접근하기 | - 주어진 CSS 변수(key)의 값을 가져와 숫자로 변환하여 반환한다. |
(b) 값 리턴하기 | - 해당하는 CSS 변수가 없으면 0을 반환한다. |
(3) init, 초기값을 지정하는 함수
init({ min, max }) {
const { MIN_VALUE, MAX_VALUE, RANGE_STEP } = this.constants;
const { minRange, maxRange } = this.elements;
minRange.min = maxRange.min = MIN_VALUE;
minRange.max = maxRange.max = MAX_VALUE;
minRange.step = maxRange.step = RANGE_STEP;
minRange.value = min;
maxRange.value = max;
this.setStartValue(min);
this.setEndValue(max);
}
init
함수는 주어진min
,max
값에 따라 슬라이드의 초기값을 설정한다.
(4) setHandlePos, 핸들의 위치를 조정하는 함수
setHandlePos(range, handle) {
const { MIN_VALUE, RANGE, HANDLE_SIZE } = this.constants;
// (a)
const percentage = (range.value - MIN_VALUE) / RANGE;
// (b)
const offset = HANDLE_SIZE / 2 - HANDLE_SIZE * percentage;
const left = `calc(${percentage * 100}% + ${offset}px)`;
handle.style.left = left;
}
setHandlePos
함수는range
값을 기반으로, 핸들 요소(handle
)의 위치를 설정한다.
코드 번호 | 코드 설명 |
---|---|
(a) 위치 퍼센트 계산 | range 값이 전체 범위에서 몇 퍼센트에 해당하는지 계산한다. |
(b) 핸들 위치 조정 | 핸들 크기(offset)를 고려해, 핸들의 위치(left )를 결정한다. |
(5) setStartValue, 슬라이드 최솟값을 설정하는 함수
setStartValue(v) {
const { minRange, maxRange, progress, handles } = this.elements;
// (a)
if (v >= +maxRange.value) {
v = +maxRange.value - this.constants.RANGE_STEP;
minRange.value = v;
}
// (b)
const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
progress.style.left = `${(value / this.constants.RANGE) * 100}%`;
this.setHandlePos(minRange, handles[0]);
}
setStartValue()
는 주어진v
값을 기반으로 슬라이더의 최솟값을 설정한다.
코드 번호 | 코드 설명 |
---|---|
(a) 최솟값 조정 | 최소값이 최대값보다 크거나 같으면, 최소값을 최대값보다 작게 조정한다. |
(b) 핸들 위치 조정 | 최소값 핸들의 위치를 재설정한다. |
(6) setEndValue, 슬라이드 최댓값을 설정하는 함수
setEndValue(v) {
const { minRange, maxRange, progress, handles } = this.elements;
// (a)
if (v <= +minRange.value) {
v = +minRange.value + this.constants.RANGE_STEP;
maxRange.value = v;
}
// (b)
const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
progress.style.right = `${100 - (value / this.constants.RANGE) * 100}%`;
this.setHandlePos(maxRange, handles[1]);
}
setStartValue()
는 주어진v
값을 기반으로 슬라이더의 최댓값을 설정한다.
코드 번호 | 코드 설명 |
---|---|
(a) 최댓값 조정 | 최대값이 최소값보다 작거나 같으면, 최대값을 최소값보다 크게 조정한다. |
(b) 핸들 위치 조정 | 최대값 핸들의 위치를 재설정한다. |
💡 일련의 과정을 거치면, 최소&최대값을 지정할 수 있는 RangeSlider(범위 슬라이드)가 완성된다.
See the Pen RangeSlider by KumJungMin (@kumjungmin) on CodePen.
3. 마치며…
이번 시간에는 두 개의 range input을 겹치는 방식으로, RangeSlider(범위 슬라이더)를 구현해보았다. 전반적인 설정값은 CSS 변수로 선언해 이 값에 따라 슬라이더 스타일은 물론, 슬라이드 기본값을 지정할 수 있었다.
스크립트 단에는 범위 슬라이드용 클래스를 만들어 constructor
는 초기값을 지정하고, init
은 슬라이더의 초기 값을 설정할 수 있었다.
그리고 Input
이벤트가 발생할 때, 입력값에 따라 슬라이드의 핸들 위치를 조정했다.
현재는 순수 JS로 구현을 했지만, Vue, React로 구현할 시 constructor
에 선언한 변수는 props나 반응형 변수로 지정하면 확장성 있게 재구현할 수 있을 것이다 🙂
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
[JS] event.target vs event.currentTarget의 차이 (with. vue slot) (0) | 2024.09.08 |
---|---|
[CSS] clip-path로 별점(Rating) UI 구현하는 법 (with. vue 컴포넌트 예시 포함) (13) | 2024.08.28 |
[JS] 모달 창 띄울 때 레이아웃 깨짐 없이 스크롤 막는 법 (0) | 2024.06.11 |
[TS] 입력 유효성을 체크하는 여러 정규식(feat. 한글, 전화번호, 사업자등록번호, 이메일 등) (0) | 2024.05.13 |
[CSS] 확장자 제외하고, 파일명만 말줄임하는 방법(with. input file) (2) | 2024.05.05 |
댓글