스크롤 바 커스터마이징
✒️ 2025-05-16 12:36 내용 수정
참고 자료 : Digital Ocean How to Customize Scrollbars with CSS: Styling Guide with Examples, mdn web docs scrollbar
- 프로젝트에서 스크롤 바를 추가했는데, 스크롤 바의 기본 디자인이 마음에 들지 않아 변경하는 방법을 찾아보았다.
- 유투브 클론 코딩 프로젝트로, 스타일은 유투브의 스타일을 기준으로 제작했다.
scroll과scrollbar에 연관된 속성을 사용하거나, 가상 요소::-webkit-scrollbar를 사용하면 스크롤바의 스타일을 변경할 수 있다.::-webkit-scrollbar는 Chrome, Edge, Safari에서 지원한다.
- 스크롤을 지원하는 요소(
<div>,<p>등)를 생성하고,overflow: scroll속성을 추가하면 스크롤 바를 만들 수 있다.
<div id="main-content">
<p>내용 테스트. 긴 글로 테스트하는 것을 추천합니다</p>
</div>
#main-content { overflow-x: scroll; }
::-webkit-scrollbar: 스크롤 바 전체 영역::-webkit-scrollbar-button: : 스크롤 바의 화살표 버튼으로, 스크롤바 이동에 사용한다.::-webkit-scrollbar-thumb: 스크롤바의 드래그 가능한 핸들::-webkit-scrollbar-button: 스크롤바의 진행도 영역

| 속성 | 설명 |
|---|---|
scroll-behavior |
네비게이션이나 CSSOM 스크롤링 API 사용 시 스크롤방식을 지정auto : 기본으로 지정된 위치로 바로 이동한다.smooth : 스크롤이 부드럽게 이동한다. |
scroll-padding,scroll-margin |
스크롤 요소들 사이에 시각적으로 보이는 영역의 오프셋 설정 각각 다른 요소들의 padding과 margin 속성 같은 역할0 : 스크롤이 내려가면 타겟 요소가 화면에 꽉 참0 이상 : 스크롤이 내려가면 이전 요소가 화면에 일부 보임 |
scrollbar-gutter |
스크롤바의 gutter 설정 내부 경계와 외부 padding 사이의 공간 지정 |
scrollbar-color |
스크롤바의 track과 thumb 색상 지정:-webkit-scrollbar를 지원하지 않을 경우에도 사용 가능 |
scrollbar-widthscrollbar-height |
스크롤바 두께 지정:-webkit-scrollbar를 지원하지 않을 경우에도 사용 가능 |
- 프로젝트에선
scroll과scrollbar속성으로 스타일을 변경했다.
@media (width < 472px) {
#main-content {
overflow-x: scroll;
scroll-behavior: smooth;
scrollbar-color: #AAAAAA transparent;
scroll-padding: 1px;
}
}
커스텀 스크롤 메뉴 버튼
- 프로젝트에서 스크롤 바를 보여주지 않고 커스텀 버튼을 눌렀을 때 메뉴 탭이 좌우로 스크롤 되는 요소를 만들었다.
- 버튼을 한 번 누르거나 버튼을 누르고 있으면 스크롤이 넘어간다.
- 스크롤이 왼쪽 끝에 도달하면 왼쪽 버튼을 숨기고, 오른쪽 끝에 도달하면 오른쪽 버튼을 숨긴다.
- 전체적인 코드 로직은 ChatGPT로 구현하고, 세부 내용을 수정했다.



<nav id="channel-nav">
<!-- 왼쪽 버튼 영역 -->
<div id="left-btn-wrap" class="channel-nav-btn-wrap">
<button id="ch-nav-left-btn">
<svg></svg>
</button>
</div>
<!-- 메뉴 탭 -->
<div id="channel-nav-str">
<ul id="channel-nav-list">
<li class="select">HOME</li>
<li>VIDEOS</li>
<li>PLAYLISTS</li>
<li>COMMUNITY</li>
<li>CHANNELS</li>
<li>ABOUT</li>
</ul>
<button type="submit" id="search-submit-btn">
<img src="../images/search.svg" alt="검색돋보기이미지">
</button>
</div>
<!-- 오른쪽 버튼 영역 -->
<div id="right-btn-wrap" class="channel-nav-btn-wrap">
<button id="ch-nav-right-btn">
<svg></svg>
</button>
</div>
</nav>
- 스크롤 대상 요소인
#channel-nav-str에는overflow-x: auto가 설정되어 있어 자식 요소가 길어지는 경우 x축 방향으로 스크롤이 생긴다.
#channel-nav-str {
display: flex;
scroll-behavior: smooth;
overflow-x: auto;
}
스크롤 끝에 도달 시 버튼 숨기기
DOMContentLoaded이벤트를 사용해서 HTML 문서가 완전히 파싱되면 이벤트를 추가하도록 설정한다.- 이벤트#기타 이벤트의
DOMContentLoaded참고.
- 이벤트#기타 이벤트의
document.addEventListener("DOMContentLoaded", function () {})
- 먼저 스크롤이 왼쪽 끝이나 오른쪽 끝으로 가면 왼쪽 버튼이나 오른쪽 버튼이 표시되지 않도록 설정한다.
| 속성 | 설명 |
|---|---|
scrollLeft |
현재 스크롤의 위치 왼쪽으로부터 떨어진 픽셀 수 |
scrollWidth |
스크롤 가능한 요소의 전체 길이 보이지 않는 부분도 포함 |
clientWidth |
요소의 내부 폭 길이inline 요소는 0을 반환실제 보이는 스크롤 길이 |
const nav_container = document.getElementById("channel-nav-str");
const left_btn_wrap = document.getElementById("left-btn-wrap");
const right_btn_wrap = document.getElementById("right-btn-wrap");
const left_btn = document.getElementById("ch-nav-left-btn");
const right_btn = document.getElementById("ch-nav-right-btn");
// 버튼 표시 설정
function update_btn_visibility() {
const scroll_left = nav_container.scrollLeft;
const max_scroll_left = nav_container.scrollWidth - nav_container.clientWidth;
// 화면이 클 때는 버튼이 안보이게 설정
if (nav_container.scrollWidth <= nav_container.clientWidth) {
left_btn_wrap.style.display = "none";
right_btn_wrap.style.display = "none";
return;
}
// 맨 왼쪽이면 left 버튼 숨김
left_btn_wrap.style.display = scroll_left <= 0 ? "none" : "flex";
// 맨 오른쪽이면 right 버튼을 숨김
right_btn_wrap.style.display = scroll_left >= max_scroll_left ? "none" : "flex";
}

버튼을 누르는 동안 스크롤 이동 처리
- 커스텀 스크롤 버튼을 누르면 스크롤이 이동하고, 버튼을 누르는 동안에도 스크롤이 이동하도록 설정한다.
- 다만 현재 꾹 누르고 있을 때 한 번 누른 경우와 달리 속도가 느리게 진행되어 해당 부분을 이후에 수정할 예정이다.
- 어떨 때는 속도가 빠른데 어떨 때는 속도가 답답할 정도로 느려진다.
// 스크롤 여부
let is_scrolling = false;
// 스크롤 방향
let scroll_direction = 0;
// 마지막 스크롤 위치
let last_timestamp = null;
const scroll_speed = 50; // px per ms
// 스크롤 이동 설정
function step(timestamp) {
// 스크롤 중지
if (!is_scrolling) return;
// 스크롤 진행
if (last_timestamp !== null) {
// 마지막 위치와 이동할 위치의 차이
const delta = timestamp - last_timestamp;
// 특정 방향으로 스크롤을 이동시킴
nav_container.scrollBy({ left: scroll_direction * scroll_speed * delta });
}
// 마지막 스크롤 위치 업데이트
last_timestamp = timestamp;
requestAnimationFrame(step);
}
// 스크롤 중지
function stop_scrolling() {
is_scrolling = false;
}
// 버튼 꾹 누르면 스크롤 이동 효과
function start_scrolling(direction) {
is_scrolling = true;
scroll_direction = direction;
requestAnimationFrame(step);
nav_container.scrollBy({left: direction * scroll_speed});
}
// 초기 설정 진행
update_btn_visibility();
// 버튼 눌렀을 때 반응
left_btn.addEventListener("mousedown", () => start_scrolling(-1));
right_btn.addEventListener("mousedown", () => start_scrolling(1));
// 마우스를 떼거나 멈췄을 때
left_btn.addEventListener("mouseup", stop_scrolling);
left_btn.addEventListener("mouseleave", stop_scrolling);
right_btn.addEventListener("mouseup", stop_scrolling);
right_btn.addEventListener("mouseleave", stop_scrolling);
// 스크롤에 반응한 버튼 표시 처리
nav_container.addEventListener("scroll", update_btn_visibility);
// 화면 크기가 변동되면 버튼 표시 처리
window.addEventListener("resize", update_btn_visibility);
// DOM이 로드된 후 지연된 layout 계산을 위해 requestAnimationFrame 사용
requestAnimationFrame(update_btn_visibility);

전체 코드
// 메뉴바 이동
document.addEventListener("DOMContentLoaded", function () {
const nav_container = document.getElementById("channel-nav-str");
const left_btn_wrap = document.getElementById("left-btn-wrap");
const right_btn_wrap = document.getElementById("right-btn-wrap");
const left_btn = document.getElementById("ch-nav-left-btn");
const right_btn = document.getElementById("ch-nav-right-btn");
// 버튼 표시 설정
function update_btn_visibility() {
const scroll_left = nav_container.scrollLeft;
const max_scroll_left = nav_container.scrollWidth - nav_container.clientWidth;
// 화면이 클 때는 버튼이 안보이게 설정
if (nav_container.scrollWidth <= nav_container.clientWidth) {
left_btn_wrap.style.display = "none";
right_btn_wrap.style.display = "none";
return;
}
// 맨 왼쪽이면 left 버튼 숨김
left_btn_wrap.style.display = scroll_left <= 0 ? "none" : "flex";
// 맨 오른쪽이면 right 버튼을 숨김
right_btn_wrap.style.display = scroll_left >= max_scroll_left ? "none" : "flex";
}
let is_scrolling = false;
let scroll_direction = 0;
let last_timestamp = null;
const scroll_speed = 50; // px per ms
function step(timestamp) {
if (!is_scrolling) return;
if (last_timestamp !== null) {
const delta = timestamp - last_timestamp;
nav_container.scrollBy({ left: scroll_direction * scroll_speed * delta });
}
last_timestamp = timestamp;
requestAnimationFrame(step);
}
// 스크롤 중지
function stop_scrolling() {
is_scrolling = false;
}
// 버튼 꾹 누르면 스크롤 이동 효과
function start_scrolling(direction) {
is_scrolling = true;
scroll_direction = direction;
requestAnimationFrame(step);
nav_container.scrollBy({left: direction * scroll_speed});
}
// 초기 설정 진행
update_btn_visibility();
// 버튼 눌렀을 때 반응
left_btn.addEventListener("mousedown", () => start_scrolling(-1));
right_btn.addEventListener("mousedown", () => start_scrolling(1));
// 마우스를 떼거나 멈췄을 때
left_btn.addEventListener("mouseup", stop_scrolling);
left_btn.addEventListener("mouseleave", stop_scrolling);
right_btn.addEventListener("mouseup", stop_scrolling);
right_btn.addEventListener("mouseleave", stop_scrolling);
// 스크롤에 반응한 버튼 표시 처리
nav_container.addEventListener("scroll", update_btn_visibility);
window.addEventListener("resize", update_btn_visibility);
// DOM이 로드된 후 지연된 layout 계산을 위해 requestAnimationFrame
requestAnimationFrame(update_btn_visibility);
});