1. 소개
이 섹션은 규범적이지 않습니다.
스크롤 가능한 콘텐츠를 위한 인기 있는 UX 패러다임은 종종 콘텐츠를 페이징하거나 논리적 구역으로 나누는 방식을 사용합니다. 특히 터치 인터랙션의 경우, 사용자가 계층 구조를 탭해 탐색하는 것보다 평면적으로 배치된 많은 콘텐츠를 빠르게 패닝하는 것이 더 빠르고 쉽습니다. 예를 들어, 사용자는 앨범에서 각각의 사진을 탭하여 보는 것보다 사진 슬라이드쇼 뷰를 패닝하여 여러 사진을 보는 것이 더 쉽습니다.
하지만 터치 패닝이나 마우스 휠 스크롤과 같은 스크롤 입력의 부정확한 특성 때문에, 웹 개발자가 스크롤 경험을 잘 제어하기란 어렵습니다. 특히 콘텐츠를 페이징하는 효과를 내는 것이 어렵습니다. 예를 들어, 사용자가 패닝할 때 아이템이 화면에 일부만 보이는 어색한 스크롤 위치에 도달하기 쉽습니다.
이를 위해, 이 모듈은 스크롤 스냅 위치를 도입하여 스크롤 컨테이너의 스크롤포트가 스크롤 동작이 완료된 후 도달할 수 있는 스크롤 위치를 강제합니다.
또한, 스냅이 꺼져 있을 때에도 페이징 및 스크롤 위치를 더 잘 제어할 수 있도록, 이 모듈은 모든 스크롤 컨테이너에서 사용할 수 있는 scroll-padding 속성을 정의하여 페이징 및 스크롤-인투-뷰 동작을 위해 스크롤 컨테이너의 최적 보기 영역을 조정할 수 있습니다. 이와 유사하게 scroll-margin 속성은 어떤 박스에든 사용할 수 있으며, 스크롤-인투-뷰 동작을 위해 시각적 영역을 조정할 수 있습니다.
1.1. 모듈 상호작용
이 모듈은 [CSS2] 11.1절에서 정의된 스크롤링 UI 기능을 확장합니다.
이 모듈의 어떤 속성도 ::first-line 및 ::first-letter 의사 요소에는 적용되지 않습니다.
1.2. 값 정의
이 명세는 CSS 속성 정의 규칙을 [CSS2]에서 따르며, 값 정의 문법은 [CSS-VALUES-3]에서 사용합니다. 이 명세에서 정의되지 않은 값 타입은 CSS Values & Units [CSS-VALUES-3]에서 정의됩니다. 다른 CSS 모듈과 조합되면 이러한 값 타입의 정의가 확장될 수 있습니다.
각 속성 정의에 나열된 속성별 값 외에도, 이 명세에서 정의된 모든 속성은 CSS-wide 키워드도 속성 값으로 허용합니다. 가독성을 위해 명시적으로 반복하지 않았습니다.
2. 동기 부여 예시
img{ /* 각 사진의 중앙이 스크롤 컨테이너의 X축 중앙과 정렬되도록 지정합니다 */ scroll-snap-align: none center; } .photoGallery{ width : 500 px ; overflow-x : auto; overflow-y : hidden; white-space : nowrap; /* 스크롤 위치가 스크롤 동작이 끝날 때 항상 스냅 위치에 있어야 함을 요구합니다. */ scroll-snap-type: x mandatory; }
< div class = "photoGallery" > < img src = "img1.jpg" > < img src = "img2.jpg" > < img src = "img3.jpg" > < img src = "img4.jpg" > < img src = "img5.jpg" > </ div >

.page{ /* 각 페이지의 윗부분을 스냅에 사용할 가장자리로 정의 */ scroll-snap-align: start none; } .docScroller{ width : 500 px ; overflow-x : hidden; overflow-y : auto; /* 각 요소의 스냅 영역이 위쪽에서 100px 오프셋과 정렬되도록 지정 */ scroll-padding:100 px 0 0 ; /* 스크롤 동작이 끝날 때 스냅 위치에 가까우면 스냅 위치에서 끝나도록 유도 */ scroll-snap-type: y proximity; }
< div class = "docScroller" > < div class = "page" > 페이지 1</ div > < div class = "page" > 페이지 2</ div > < div class = "page" > 페이지 3</ div > < div class = "page" > 페이지 4</ div > </ div >

3. 스크롤 스냅 모델
이 모듈은 스크롤 스냅 위치에 대한 제어를 정의합니다.
스크롤 스냅 위치는 스크롤 컨테이너 내에서 콘텐츠가 특정 정렬을 이루는 스크롤 위치입니다.
관련 scroll-snap-type 속성을 스크롤
컨테이너에 적용하면,
저자는 스크롤 동작(프로그램적 스크롤 포함, 예: scrollTo()
메서드) 이후에
스크롤포트가 스냅 위치에 도달하도록 바이어스를 요청할 수 있습니다.
스냅 위치는 요소의 scroll-snap-align 값에 따라 특정 정렬로 지정되며, 요소의 스크롤 스냅 영역(scroll-margin으로 수정된 경계 박스)을 스크롤 컨테이너의 스냅포트(scroll-padding으로 축소된 스크롤포트) 내에서 정렬합니다. 이는 정렬 대상을 정렬 컨테이너 내에서 정렬하는 것과 개념적으로 동일합니다. 지정된 정렬을 만족하는 스크롤 위치가 스냅 위치입니다.
스크롤 컨테이너의 스크롤포트의 스크롤 위치를 스냅 위치에 맞추도록 조정하는 행위를 스냅이라고 하며, 스크롤 컨테이너가 스냅된 상태란, 스크롤포트의 스크롤 위치가 스냅 위치이며, 활성 스크롤 동작이 없는 경우를 의미합니다. CSS 스크롤 스냅 모듈은 스냅 위치를 강제하기 위해 사용되는 정확한 애니메이션이나 물리를 명시하거나 요구하지 않습니다; 이는 사용자 에이전트에 맡깁니다.
스냅 위치는 요소의 포함 블록 체인에서 가장 가까운 상위 스크롤 컨테이너에만 영향을 줍니다.
4. 스크롤 스냅 영역 캡처: 스크롤 컨테이너의 속성
4.1. 스크롤 스냅 규칙: scroll-snap-type 속성
이름: | scroll-snap-type |
---|---|
값: | none | [ x | y | block | inline | both ] [ mandatory | proximity ]? |
초기값: | none |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 지정된 키워드 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 불연속(discrete) |
scroll-snap-type 속성은 스크롤 컨테이너가 스크롤 스냅 컨테이너인지 여부, 얼마나 엄격하게 스냅되는지, 그리고 어떤 축을 고려하는지 지정합니다. 엄격성 값이 지정되지 않으면 proximity 가 기본으로 사용됩니다.
html { scroll-snap-type: block; /* 메인 문서 스크롤러에 적용 */ } h1, h2, h3, h4, h5, h6 { scroll-snap-align: start; /* 뷰포트의 시작(상단)에 스냅 */ }
UA는 루트 요소에 설정된 scroll-snap-type 값을
문서 뷰포트에 적용해야 합니다.
overflow와 달리, scroll-snap-type 값은 HTML
body
에서
전달되지 않습니다.
4.1.1. 스크롤 스냅 축: x, y, block, inline, 그리고 both 값
축 값은 어떤 축(들)이 스냅 위치의 영향을 받는지, 그리고 스냅 위치가 각 축마다 독립적으로 평가되는지, 아니면 2D 포인트로 함께 평가되는지를 지정합니다. 값은 다음과 같이 정의됩니다:
- x
- 스크롤 컨테이너는 수평축에서만 스냅하여 스냅 위치에 도달합니다.
- y
- 스크롤 컨테이너는 수직축에서만 스냅하여 스냅 위치에 도달합니다.
- block
- 스크롤 컨테이너는 block 축에서만 스냅하여 스냅 위치에 도달합니다.
- inline
- 스크롤 컨테이너는 인라인 축에서만 스냅하여 스냅 위치에 도달합니다.
- both
- 스크롤 컨테이너는 두 축 모두 독립적으로 스냅하여 스냅 위치에 도달할 수 있습니다 (각 축에서 서로 다른 요소에 스냅될 수도 있음).
4.1.2. 스크롤 스냅 엄격성: none, proximity, 그리고 mandatory 값
엄격성 값 (none, proximity, mandatory) 는 스냅 위치가 스크롤 컨테이너에서 (스크롤 위치를 강제로 조정함으로써) 얼마나 엄격하게 적용되는지 지정합니다. 값은 다음과 같이 정의됩니다:
- none
- 스크롤 컨테이너에 지정되면, 스크롤 컨테이너는 스냅하지 않습니다.
- mandatory
- 스크롤 컨테이너에 지정되면, 스크롤 컨테이너는 활성 스크롤 동작이 없을 때 반드시 스냅 위치에 스냅되어 있어야 합니다. 유효한 스냅 위치가 존재하면 스크롤 종료 시 반드시 스냅해야 합니다 (존재하지 않으면 스냅이 발생하지 않음).
- proximity
- 스크롤 컨테이너에 지정되면, 스크롤 컨테이너는 스크롤 종료 시 UA의 판단에 따라, 스크롤 파라미터에 따라 스냅 위치에 스냅할 수 있습니다.
저자는 화면 크기와(적용되는 경우) 콘텐츠 크기가 다양할 수 있으므로, mandatory 스냅 위치 사용에 신중해야 합니다. 특히 스냅된 요소가 스크롤포트보다 큰 경우 UA가 접근을 처리하지만, 인접하지 않은 형제 요소에 mandatory 스냅을 지정하면, 그 사이의 콘텐츠가 화면보다 긴 경우 접근 불가가 될 수 있습니다.
박스는 스냅 위치를 캡처한다고 하며, 스크롤 컨테이너이거나 none이 아닌 scroll-snap-type 값을 가진 경우입니다. 박스의 스냅 위치 캡처 상위 요소가 포함 블록 체인에서 non-none 값을 가진 스크롤 컨테이너라면, 그 박스가 스크롤 스냅 컨테이너입니다. 그렇지 않으면 박스는 스크롤 스냅 컨테이너가 없으며, 스냅 위치는 스냅을 트리거하지 않습니다.
4.1.3. 레이아웃 변경 후 다시 스냅
문서의 콘텐츠나 레이아웃이 변경되어 (예: 콘텐츠가 추가, 이동, 삭제, 크기 변경됨) 스냅포트의 콘텐츠가 변경되면, UA는 결과 스크롤 위치를 재평가하고 필요하면 다시 스냅해야 합니다. 스크롤 컨테이너가 콘텐츠 변경 전에 스냅된 상태이고 동일한 스냅 위치가 여전히 존재한다면 (예: 관련 요소가 삭제되지 않았음), 콘텐츠 변경 후에도 스크롤 컨테이너는 반드시 동일한 스냅 위치로 다시 스냅되어야 합니다. 여러 박스가 이전에 스냅되어 있었고 그들의 스냅 위치가 더 이상 일치하지 않는다면, 그 중 하나가 포커스되거나 타겟팅되어 있다면 스크롤 컨테이너는 그 박스로 다시 스냅해야 하며, 그렇지 않으면 어느 박스에 다시 스냅할지 UA가 결정합니다. (UA는 예를 들어, 요소가 스냅된 상태를 추적하다가 레이아웃 변화로 다른 요소의 스냅 위치가 맞거나 틀어질 때 이를 관리할 수 있습니다.)
새로운 박스나 다른 박스로 다시 스냅해야 하는 경우의 스크롤은 다른 scroll-into-view 동작과 동일한 방식 및 애니메이션을 따라야 하며, scroll-behavior와 같은 컨트롤도 준수해야 합니다. 이전과 동일한 박스로 다시 스냅하는 경우의 스크롤 동작은 UA가 정의합니다. UA는 예를 들어, 섹션의 시작에 스냅되어 있을 때 문서 앞부분에 동적으로 콘텐츠가 추가되더라도 섹션의 새로운 위치로 스크롤 애니메이션을 하지 않고 스크롤이 발생하지 않는 것처럼 보이게 할 수 있습니다.
.log{ scroll-snap-type : proximity; align-content : end; } .log::after{ display : block; content : "" ; scroll-snap-align : end; }
이 규칙들은 scroll snap area 하나를 생성하며, ::after 의사 요소로 표현됩니다. 이는 스크롤 스냅 컨테이너의 맨 하단에 위치합니다. 사용자가 하단 “근처”로 스크롤하면, 컨테이너가 해당 위치로 스냅됩니다. 컨테이너에 더 많은 콘텐츠가 동적으로 추가되어도, 계속 해당 위치에 스냅된 상태가 유지됩니다 (스크롤 컨테이너는 어떤 변경 후에도 동일한 scroll snap area가 존재하면 반드시 다시 스냅해야 하기 때문). 하지만 사용자가 로그 안의 다른 곳으로 스크롤했다면, 아무 동작도 하지 않습니다.
4.2. 스크롤 스냅포트: scroll-padding 속성
이름: | scroll-padding |
---|---|
값: | [ auto | <length-percentage> ]{1,4} |
초기값: | auto |
적용 대상: | 스크롤 컨테이너 |
상속 여부: | no |
백분율: | 스크롤 컨테이너의 스크롤포트의 해당 차원을 기준으로 상대적 |
계산된 값: | 각 측면별, auto 키워드 또는 계산된 <length-percentage> 값 |
애니메이션 타입: | 계산된 값 타입에 따라 |
정형 순서: | 문법에 따름 |
이 속성은 (스크롤 컨테이너 전체에 대해, 스크롤 스냅 컨테이너만이 아니라) 스크롤포트의 최적 보기 영역을 정의하는 오프셋을 지정합니다: 사용자가 보는 영역에 요소를 배치할 때 대상이 되는 영역입니다. 저자는 스크롤포트의 일부 영역을 다른 콘텐츠(예: 고정 위치 툴바나 사이드바)에 의해 가려진 부분에서 제외하거나, 단순히 대상 요소와 스크롤포트의 가장자리 사이에 여유 공간을 줄 수 있습니다.
scroll-padding 속성은 축약 속성으로, scroll-padding-* 롱핸드를 한 번에 설정하며, 각 측면의 롱핸드에 값을 할당하는 방식은 padding 속성과 동일합니다. 값의 의미는 다음과 같습니다:
- <length-percentage>
-
스크롤포트의 해당 가장자리에서 내부로의 오프셋을 정의합니다. 루트 뷰포트에 적용될 때, 오프셋은 레이아웃 뷰포트를 기준으로 계산 및 적용되며 (비주얼 뷰포트가 아님), inset 속성이 고정 위치 박스에서 적용되는 방식과 동일합니다; 최적 보기 영역은 남은 영역 중 비주얼 뷰포트와 교차하는 부분입니다.
- auto
-
스크롤포트의 해당 가장자리 오프셋이 UA에 의해 결정됨을 나타냅니다. 일반적으로 0px로 기본값이 설정되어야 하지만, UA는 비영(0이 아닌 값이 더 적합할 때)을 감지하는 휴리스틱을 사용할 수 있습니다.
이 오프셋들은 스크롤포트의 영역 중 스크롤 동작에서 “보기 가능”으로 간주되는 영역을 줄입니다: 레이아웃에는 영향을 주지 않으며, 스크롤 원점이나 초기 위치에도 영향을 주지 않고, 요소가 실제로 보이는지 여부에도 영향을 주지 않습니다. 하지만 요소나 캐럿이 스크롤되어 보이는 상태로 간주되는지(예: 타겟팅 또는 포커싱 작업에서), 페이징 동작(예: PgUp, PgDn 키 사용 또는 스크롤바에서 동등 동작 트리거)에서 스크롤되는 양을 줄여야 하며, 최적 보기 영역 내에서 스크롤포트가 사용자에게 연속적인 콘텐츠 스트림을 보여줍니다.
스크롤 스냅 컨테이너에서는
이 영역이
스크롤
스냅포트도 정의합니다—
html{ overflow-x : auto; overflow-y : hidden; scroll-snap-type : x mandatory; scroll-padding : 0 500 px 0 0 ; } .toolbar{ position : fixed; height : 100 % ; width : 500 px ; right : 0 ; } img{ scroll-snap-align : none center; }
UA는 루트 요소에 설정된 scroll-padding 값을
문서 뷰포트에 적용해야 합니다.
(overflow와 달리, scroll-padding 값은 HTML
body
에서
전달되지 않습니다.)
5. 스크롤 스냅 영역 정렬: 요소의 속성
5.1. 스크롤 스냅 영역: scroll-margin 속성
이름: | scroll-margin |
---|---|
값: | <length>{1,4} |
초기값: | 0 |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 각 측면별 절대 길이 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 계산된 값 타입에 따라 |
이 속성은 축약 속성으로, scroll-margin-* 롱핸드를 한 번에 설정하며, 각 측면의 롱핸드에 값을 할당하는 방식은 margin 속성과 동일합니다.
값은 이 박스를 스냅포트에 스냅할 때 사용되는 스크롤 스냅 영역을 정의하는 외측 오프셋입니다. 스크롤 스냅 영역은 변형된 border 박스를 가져와 스크롤 컨테이너의 좌표 공간에서 축에 맞춘(축에 정렬된) 사각형 경계 박스를 찾은 뒤, 지정된 외측 오프셋을 더하여 결정됩니다.
참고: 이렇게 하면 스크롤 스냅 영역이 항상 사각형이며 스크롤 컨테이너의 좌표 공간에 맞게 축 정렬됩니다.
페이지가 프래그먼트로 탐색되어 타겟 요소가 정의되면
(:target으로 매칭되거나,
scrollIntoView()
의
대상인 경우),
UA는 해당 요소의 스크롤 스냅 영역을
사용해야 하며,
단순 border 박스만이 아니라,
스크롤 가능한 오버플로우 영역 중 어느 영역을 보여줄지 판단할 때 사용해야 합니다. 스냅이 꺼져
있거나
이 요소에 적용되지 않아도 마찬가지입니다.
5.2. 스크롤 스냅 정렬: scroll-snap-align 속성
이름: | scroll-snap-align |
---|---|
값: | [ none | start | end | center ]{1,2} |
초기값: | none |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 두 개의 키워드 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 불연속(discrete) |
scroll-snap-align 속성은 박스의 스냅 위치를 스냅 영역의 정렬로 지정합니다 (정렬 대상으로). 박스의 스냅 컨테이너의 스냅포트 내에서(정렬 컨테이너로), 두 값은 각각 block 축과 inline 축의 스냅 정렬을 지정하며, 스냅 컨테이너의 writing mode에 따라 결정됩니다. 한 값만 지정되면, 두 번째 값은 첫 번째 값과 동일하게 기본값이 설정됩니다.
값은 다음과 같이 정의됩니다:
- none
- 이 박스는 지정된 축에 스냅 위치를 정의하지 않습니다.
- start
- 이 박스의 스크롤 스냅 영역을 스크롤 컨테이너의 스냅포트의 시작에 정렬하면 지정된 축에서 스냅 위치가 됩니다.
- end
- 이 박스의 스크롤 스냅 영역을 스크롤 컨테이너의 스냅포트의 끝에 정렬하면 지정된 축에서 스냅 위치가 됩니다.
- center
- 이 박스의 스크롤 스냅 영역을 스크롤 컨테이너의 스냅포트의 중앙에 정렬하면 지정된 축에서 스냅 위치가 됩니다.
start와 end 정렬은 writing mode에 따라 스냅 컨테이너 기준으로 해석되며, 스크롤 스냅 영역이 스냅포트보다 큰 경우에는 박스 자체의 writing mode 기준으로 해석됩니다. (이렇게 하면 컨테이너 내 아이템의 스냅 정렬이 일반적으로 일관성을 가지면서도, start가 항상 아이템의 시작이 읽기 편하도록 정렬됨을 보장할 수 있습니다.)
5.2.1. 유효한 스냅 위치의 범위 지정: 화면에 보이는 박스에 한정
스크롤 스냅의 목적이 스크롤포트 내의 콘텐츠를 최적의 보기로 정렬하는 것이므로, 스크롤 위치가 해당 스냅 위치로 스냅될 경우, 관련 스냅 영역이 전체적으로 스냅포트 밖에 위치한다면, 그 위치는 유효한 스냅 위치로 간주될 수 없습니다. (비록 스냅 영역의 정렬 조건을 만족하더라도)
╔════viewport════╗┈┈┈┈┈┈┈┈┌──────────────┐ ║ ┌─────┐ ┌──┐ ║ │ top-snapping │ ║ ├──┐ │ └──┘ ║ │ element │ ║ └──┴──┘ ║ │ │ ╚════════════════╝ │ │ └──────────────┘
왜 스냅을 요소가 화면에 보일 때만 제한하나요?
WebKit 구현자들이 지적한 것처럼, 스냅 엣지를 캔버스 전체에 무한히 확장하면 격자 레이아웃만 스냅할 수 있게 되고, 화면 밖 요소가 화면 안 요소와 정렬되지 않을 때 사용자가 이상한 동작을 경험하게 됩니다. (만약 이 요구사항이 구현자에게 부담스럽다면, 기본적으로 격자 기반 동작으로 하고, 나중에 더 스마트한 동작을 위한 스위치를 도입할 수 있습니다.)참고: scroll-snap-type: both는 스냅 위치를 각 축에서 독립적으로 평가하지만, 한 축에서 스냅 위치를 선택할 때 다른 축의 스냅 위치의 영향도 받을 수 있습니다. 예를 들어, 한 축에서 스냅하면 다른 축이 정렬하려던 스냅 영역이 화면 밖으로 밀려나 해당 스냅 위치가 무효가 되어 선택할 수 없게 됩니다.
5.2.2. 스크롤포트를 넘치는 박스의 스냅
스냅 영역이 특정 축에서 스냅포트보다 크면, 그 축에서 스냅 영역이 스냅포트를 덮는 모든 스크롤 위치와, 해당 축에서 이전/다음 스냅 위치 사이의 거리가 스냅포트보다 크면, 그 위치는 해당 축에서 유효한 스냅 위치입니다. UA는 특정 스크롤 동작(예: 명시적 페이징)에 대해 지정된 정렬을 더 정확한 대상으로 사용할 수 있습니다.
스냅 영역이 스냅포트보다 크므로, 영역이 뷰포트를 가득 채울 때는 컨테이너를 자유롭게 스크롤할 수 있고 정렬 위치로 다시 스냅되지 않습니다. 하지만 컨테이너가 스크롤되어 영역이 축에서 뷰포트를 완전히 채우지 않게 되면, 영역은 바깥쪽으로 스크롤되는 것을 막으려고 하고, 충분히 스크롤되면 다른 스냅 위치로 스냅됩니다.
section
요소에 mandatory top 스냅을 적용하면
(상위 section이 큰 경우)
큰 스냅 영역이 만들어지고,
그 안에 더 작은 스냅 영역(하위 section)들이 들어갈 수 있습니다.
하위 section이 충분히 작으면
정상적으로 스냅되고,
길면
해당 영역 내에서 자유롭게 스크롤하거나,
하위 section이 없는 상위 section의 큰 영역을 자유롭게 스크롤할 수 있습니다.
┌─ top-level section ─┐ ━┓ │ │ 1┃ │ │ ┃ │ │ ━┩ │ │ ┆ │ │ ┆ │┌─── sub-section ───┐│ ╯ ━┓ │└───────────────────┘│ 2┃ │┌─── sub-section ───┐│ ━┓ ┃ ││ ││ 3┃ ━┛ │└───────────────────┘│ ┃ │┌─── sub-section ───┐│ ━┛ ━┓ │└───────────────────┘│ 4┃ │┌─── sub-section ───┐│ ━┓ ┃ ││ ││ 5┃ ━┛ ││ ││ ┃ ││ ││ ━┩ ││ ││ ┆ ││ ││ ┆ ││ ││ ┆ │└───────────────────┘│ ┆ └─────────────────────┘ ╯
참고: 만약 저자가 각 section의 제목에 mandatory 스냅을 설정했다면 (section 자체가 아니라), 첫 번째와 다섯 번째 section의 콘텐츠 일부가 사용자에게 접근 불가가 됩니다. 헤딩 스냅 영역이 전체 section을 덮지 않기 때문입니다. 이런 이유로, 멀리 떨어진 요소에 mandatory 스냅을 사용하는 것은 바람직하지 않습니다.
5.2.3. 도달 불가능한 스냅 위치
스냅 위치가 지정된 대로 도달 불가능하므로, 해당 위치로 정렬하려면 스크롤 컨테이너의 뷰포트를 스크롤 가능한 오버플로우 영역의 가장자리 너머로 스크롤해야 할 경우, 이 스냅 영역의 사용된 스냅 위치는 원하는 스냅 위치 방향으로 각 축에서 최대한 스크롤한 위치입니다.
5.3. 스크롤 스냅 한계: scroll-snap-stop 속성
이름: | scroll-snap-stop |
---|---|
값: | normal | always |
초기값: | normal |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 지정된 키워드 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 불연속(discrete) |
의도된 방향으로 스크롤할 때, 스크롤 컨테이너는 여러 스냅 위치를 “넘어갈 수 있습니다” (같은 방향이지만 더 짧은 거리였다면 스냅될 수 있었던 위치), 스크롤 동작의 자연스러운 끝점에 도달해 최종 스크롤 위치를 선택하기 전까지. scroll-snap-stop 속성을 사용하면, 이런 스냅 위치에서 스크롤 동작을 “트랩”할 수 있어, 스크롤 컨테이너가 스크롤 동작이 자연스럽게 끝나기 전에 멈추도록 강제할 수 있습니다.
값은 다음과 같이 정의됩니다:
- normal
- 스크롤 컨테이너는 스크롤 동작 중 이 요소가 정의한 스냅 위치를 넘어갈 수 있습니다.
- always
- 스크롤 컨테이너는 스크롤 동작 중 이 요소가 정의한 스냅 위치를 넘어가면 안 되며, 대신 이 요소의 첫 번째 스냅 위치에 반드시 스냅해야 합니다.
이 속성은 의도된 끝 위치만 있는 스크롤 동작에는 영향을 주지 않습니다. 이런 경우에는 어떤 스냅 위치도 개념적으로 “넘어가는” 일이 없기 때문입니다.
6. 스냅 동작 메커니즘
어떤 스냅 위치로 스냅할지 선택하는 정확한 모델 알고리즘은 일부러 대부분 정의하지 않았습니다. 이렇게 하면 사용자 에이전트가 사용자의 의도 및 상호작용을 정교하게 모델링하고, 시간이 지남에 따라 반응 방식을 조정하여 사용자에게 최적의 경험을 제공할 수 있습니다.
이 섹션에서는 스크롤 스냅 동작을 논의하는 데 도움이 되는 몇 가지 개념을 정의하고, 효과적인 스크롤 스냅 전략이 어떤 모습일지에 대한 가이드라인을 제공합니다. 사용자 에이전트는 이 지침을 참고하되 자신만의 최선의 판단을 적용해 스냅 동작을 정의하는 것이 권장됩니다. 또한, 저자가 스크롤 스냅을 염두에 두고 인터페이스를 설계할 때 최소한의 합리적인 동작을 보장하기 위한 몇 가지 동작 요구사항도 포함되어 있습니다.
6.1. 스크롤 방식의 종류
페이지가 스크롤될 때, 스크롤은 의도된 끝 위치와/또는 의도된 방향을 가집니다. 이 두 가지의 조합마다 서로 다른 스크롤 카테고리가 정의되며, 각각 약간 다르게 처리할 수 있습니다:
- 의도된 끝 위치
-
의도된 끝 위치만 있는 스크롤의 흔한 예시는 다음과 같습니다:
-
모멘텀 없이 릴리즈된 패닝 제스처
-
스크롤바 “썸”을 직접 조작
-
scrollTo()
와 같은 API로 프로그래밍적으로 스크롤 -
문서의 포커스 가능한 요소를 탭으로 이동
-
페이지 내 앵커로 네비게이션
-
Home/End 키와 같은 홈 동작
-
- 의도된 방향 및 끝 위치
-
의도된 방향 및 끝 위치가 모두 있는 스크롤의 흔한 예시는 다음과 같습니다:
-
모멘텀이 적용되는 “플링” 제스처
-
scrollBy()
와 같은 API로 프로그래밍적으로 스크롤 -
PgUp/PgDn 키(또는 스크롤바에서 동등 동작)로 페이징
스냅 포인트 등 기능 개입 이전의 스크롤 의도된 끝점은 자연스러운 끝점입니다.
-
- 의도된 방향
또한, 페이지 레이아웃이 일반적으로 수직/수평 정렬이기 때문에, UA는 스크롤의 방향이 충분히 수직/수평이면 축 고정(axis-lock)을 적용할 수 있습니다. 축이 고정된 스크롤은 해당 축만 따라 이동합니다. 이렇게 하면 덜 정밀한 입력장치가 비주 축으로 드리프트하는 것을 방지할 수 있습니다.
참고: 이 명세는 UA가 지원하는 스크롤 방식에만 적용됩니다; UA가 특정 입력이나 스크롤 방식을 반드시 지원해야 하는 것은 아닙니다.
6.2. 스냅 위치 선택
스크롤 컨테이너에는 스냅 영역이 스크롤 가능한 오버플로우 영역에 여러 군데 흩어져 있을 수 있습니다. 스냅 위치를 선택하는 단순한 알고리즘은 사용자가 직관적으로 이해하기 힘든 동작을 만들 수 있으므로, 선택 알고리즘 설계 시 주의가 필요합니다. 다음은 선택 과정에 도움이 될 수 있는 팁입니다:
-
스냅 위치는 끝점 (또는 자연스러운 끝점) 과 최종 스냅 스크롤 위치 사이의 거리를 최소화하도록 선택해야 합니다. 단, 이 섹션의 추가 제약을 만족해야 합니다.
-
스크롤이 축 고정 상태라면, 다른 축의 스냅 위치는 스크롤 중 무시해야 합니다. (단, 다른 축의 스냅 위치가 최종 스크롤 위치에는 영향을 줄 수 있습니다.)
-
화면에서 멀리 떨어진 요소가 스크롤 위치에 이해하기 어려운 영향을 주지 않도록, 스냅 위치의 요소가 스냅포트가 스크롤 가능한 오버플로우 영역을 따라 이동할 때 정의되는 “통로(corridor)” 바깥에 있거나, 오직 의도된 방향만 있는 스크롤의 가상 “통로” 바깥에 있거나, 의도된 끝 위치만 있는 스크롤 이후의 스냅포트 바깥에 있다면 그런 스냅 위치는 무시해야 합니다.
-
사용자 에이전트는 어떤 스크롤 방식이든 사용자가 스냅 위치에서 “탈출”할 수 있게 반드시 보장해야 합니다. 예를 들어, 스냅 타입이 mandatory이고 다음 스냅 위치가 두 화면 너비보다 멀리 있다면, 단순히 “항상 가장 가까운 위치로 스냅”하는 알고리즘은 사용자가 끝 위치를 한 화면 너비만큼 이동하고 싶어도 스냅 위치에 갇혀버릴 수 있습니다. 대신, 끝점이 시작 스냅 위치와 충분히 가까울 때만 그 위치로 돌아가고, 그 외에는 시작 스냅 위치를 무시하는 똑똑한 알고리즘이 더 나은 결과를 만듭니다.
-
페이지가 프래그먼트로 탐색되어 타겟 요소가 정의되고 (예: :target으로 매칭되거나,
scrollIntoView()
의 대상인 경우), 그 요소가 스냅 위치를 정의한다면, UA는 가장 가까운 스크롤 컨테이너가 스크롤 스냅 컨테이너일 경우 반드시 그 요소의 스냅 위치 중 하나로 스냅해야 합니다. UA는 스크롤 컨테이너에 scroll-snap-type: none이 설정된 경우에도 이를 수행할 수 있습니다.
부록 A: 롱핸드
물리적 및 논리적 롱핸드(및 해당 축약형)는 [CSS-LOGICAL-1]에서 정의된 대로 상호작용합니다.
물리적 롱핸드(scroll-padding)
이름: | scroll-padding-top, scroll-padding-right, scroll-padding-bottom, scroll-padding-left |
---|---|
값: | auto | <length-percentage> |
초기값: | auto |
적용 대상: | 스크롤 컨테이너 |
상속 여부: | no |
백분율: | 스크롤 컨테이너의 스크롤포트 기준 |
계산된 값: | auto 키워드 또는 계산된 <length-percentage> 값 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 계산된 값 타입에 따라 |
scroll-padding의 이 롱핸드들은 각각 스냅포트의 상, 우, 하, 좌 측면을 지정합니다. 음수 값은 허용되지 않습니다.
플로우 상대 롱핸드(scroll-padding)
이름: | scroll-padding-inline-start, scroll-padding-block-start, scroll-padding-inline-end, scroll-padding-block-end |
---|---|
값: | auto | <length-percentage> |
초기값: | auto |
적용 대상: | 스크롤 컨테이너 |
상속 여부: | no |
백분율: | 스크롤 컨테이너의 스크롤포트 기준 |
계산된 값: | auto 키워드 또는 계산된 <length-percentage> 값 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 계산된 값 타입에 따라 |
scroll-padding의 이 롱핸드들은 각각 스냅포트의 block-start, inline-start, block-end, inline-end 측면을 지정합니다. 음수 값은 허용되지 않습니다.
이름: | scroll-padding-block, scroll-padding-inline |
---|---|
값: | [ auto | <length-percentage> ]{1,2} |
초기값: | auto |
적용 대상: | 스크롤 컨테이너 |
상속 여부: | no |
백분율: | 스크롤 컨테이너의 스크롤포트 기준 |
계산된 값: | 각 개별 속성 참조 |
애니메이션 타입: | 계산된 값 기준 |
정형 순서: | 문법에 따름 |
축약 속성 scroll-padding-block-start + scroll-padding-block-end, scroll-padding-inline-start + scroll-padding-inline-end는 scroll-padding의 롱핸드들이며, 각각 스냅포트의 block 축과 inline 축의 측면을 지정합니다.
값이 두 개 지정되면, 첫 번째 값이 시작 값이고 두 번째 값이 끝 값입니다.
scroll-margin의 물리적 롱핸드
이름: | scroll-margin-top, scroll-margin-right, scroll-margin-bottom, scroll-margin-left |
---|---|
값: | <length> |
초기값: | 0 |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 절대 길이 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 계산된 값 타입 기준 |
scroll-margin의 이 롱핸드들은 각각 scroll-margin의 상, 우, 하, 좌 측면을 스크롤 스냅 영역에 지정합니다.
scroll-margin의 플로우 상대 롱핸드
이름: | scroll-margin-block-start, scroll-margin-inline-start, scroll-margin-block-end, scroll-margin-inline-end |
---|---|
값: | <length> |
초기값: | 0 |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 절대 길이 |
정형 순서: | 문법에 따름 |
애니메이션 타입: | 계산된 값 타입 기준 |
scroll-margin의 이 롱핸드들은 각각 scroll-margin의 block-start, inline-start, block-end, inline-end 측면을 스크롤 스냅 영역에 지정합니다.
이름: | scroll-margin-block, scroll-margin-inline |
---|---|
값: | <length>{1,2} |
초기값: | 0 |
적용 대상: | 모든 요소 |
상속 여부: | no |
백분율: | 해당 없음 |
계산된 값: | 개별 속성 참조 |
애니메이션 타입: | 계산된 값 타입 기준 |
정형 순서: | 문법에 따름 |
scroll-margin-block-start + scroll-margin-block-end, scroll-margin-inline-start + scroll-margin-inline-end는 scroll-margin의 롱핸드들이며, 각각 스크롤 스냅 영역의 block 축과 inline 축의 측면을 지정합니다.
값이 두 개 지정되면, 첫 번째 값이 시작 값이고 두 번째 값이 끝 값입니다.
7. 개인정보 및 보안 고려사항
이 명세는 DOM에 이미 직접 노출된 정보 외에 다른 정보를 노출하지 않습니다; 단지 스크롤 기능을 조금 더 향상시킬 뿐입니다. 새로운 개인정보 또는 보안 고려사항은 없습니다.
8. 감사의 글
David Baron, Simon Fraser, Håkon Wium Lie, Theresa O’Connor, François Remy, Majid Valpour, 그리고 특히 Robert O’Callahan에게 제안과 권고에 깊은 감사를 드립니다. 그들의 의견이 이 문서에 반영되었습니다.
9. 변경 사항
9.1. 2019년 3월 19일 CR 이후 변경 사항
2019년 3월 19일 후보 권고 이후의 변경 사항은 다음과 같습니다:
- scroll-snap-align이 해석될 때 어떤 writing mode가 사용되는지 명시했습니다. (이슈 3815)
-
여러 요소가 일치할 때 다시 스냅하는 요구사항을 정의했습니다.
(이슈 4651)
콘텐츠 변경 전에 스크롤 컨테이너가 스냅된 상태였고 동일한 스냅 위치가 여전히 존재하면 (예: 관련 요소가 삭제되지 않음), 콘텐츠 변경 후에도 반드시 동일한 스냅 위치로 다시 스냅해야 합니다. 여러 박스가 이전에 스냅되어 있었고 그들의 스냅 위치가 더 이상 일치하지 않으면, 그 중 하나가 포커스되거나 타겟팅되어 있다면 스크롤 컨테이너는 그 박스로 다시 스냅해야 하며, 그렇지 않으면 어느 박스에 다시 스냅할지 UA가 결정합니다. (UA는 예를 들어, 요소가 스냅된 상태를 추적하다가 레이아웃 변화로 다른 요소의 스냅 위치가 맞거나 틀어질 때 이를 관리할 수 있습니다.)
-
새로운 요소로 다시 스냅할 때는 다른 scroll-into-view 동작과
동일하게 애니메이션되어야 함을 요구합니다.
(이슈 4609)
새로운 박스나 다른 박스로 다시 스냅해야 하는 경우의 스크롤은 다른 scroll-into-view 동작과 동일한 방식 및 애니메이션을 따라야 하며, scroll-behavior와 같은 컨트롤도 준수해야 합니다. 이전과 동일한 박스로 다시 스냅하는 경우의 스크롤 동작은 UA가 정의합니다. UA는 예를 들어, 섹션의 시작에 스냅되어 있을 때 문서 앞부분에 동적으로 콘텐츠가 추가되어도 섹션의 새로운 위치로 스크롤 애니메이션을 하지 않고 스크롤이 발생하지 않는 것처럼 보이게 할 수 있습니다.
-
scroll-snap-type 및 scroll-padding 값이 루트 요소에서
문서 뷰포트로 예상대로 전달됨을 명시적으로 정의했습니다.
(이슈 3740)
UA는 루트 요소에 설정된 scroll-snap-type 값을 문서 뷰포트에 적용해야 합니다. overflow와 달리, scroll-snap-type 값은 HTML
body
에서 전달되지 않습니다.UA는 루트 요소에 설정된 scroll-padding 값을 문서 뷰포트에 적용해야 합니다. (overflow와 달리, scroll-padding 값은 HTML
body
에서 전달되지 않습니다.) -
snap 정렬은 visual viewport 기준이지만,
scroll-padding은 layout viewport를 기준으로 해석되어
루트 뷰포트에서 scroll-padding과 inset이 일관성이 있도록 명확하게 했습니다.
(이슈 4393)
스크롤포트의 해당 가장자리에서 내부로의 오프셋을 정의합니다. 루트 뷰포트에 적용될 때, 오프셋은 레이아웃 뷰포트를 기준으로 계산 및 적용되며 (비주얼 뷰포트가 아님), inset 속성이 고정 위치 박스에서 적용되는 방식과 동일합니다; 최적 보기 영역은 남은 영역 중 비주얼 뷰포트와 교차하는 부분입니다.
- scroll-padding-inline과 scroll-padding-block의 “적용 대상” 줄을 수정했습니다. (이슈 5845)
의견 처리 결과(Disposition of Comments)를 확인할 수 있습니다.
9.2. 2019년 1월 31일 CR 이후 변경 사항
2019년 1월 31일 후보 권고 이후의 변경 사항은 다음과 같습니다:
-
scroll-padding과 scroll-margin이 스크롤 스냅이
꺼져 있어도 적용됨을 강조했습니다.
(이슈 3721)
또한 스냅이 꺼져 있을 때에도 페이징 및 스크롤 위치를 더 잘 제어할 수 있도록, 이 모듈은 모든 scroll-padding 속성을 스크롤 컨테이너 전체에 사용할 수 있도록 정의하며, 페이징 및 스크롤-인투-뷰 동작을 위해 스크롤 컨테이너의 최적 보기 영역을 조정할 수 있습니다; 이와 유사하게 scroll-margin 속성은 어떤 박스에든 사용할 수 있으며, 스크롤-인투-뷰 동작을 위해 시각적 영역을 조정할 수 있습니다.
이 속성은 (스크롤 컨테이너 전체에 대해, 스크롤 스냅 컨테이너만이 아니라) 스크롤포트의 최적 보기 영역을 정의하는 오프셋을 지정합니다…
페이지가 프래그먼트로 탐색되어 타겟 요소가 정의되면 (:target으로 매칭되거나,
scrollIntoView()
의 대상인 경우), UA는 해당 요소의 스크롤 스냅 영역을 사용해야 하며, 단순 border 박스만이 아니라, 스크롤 가능한 오버플로우 영역 중 어느 영역을 보여줄지 판단할 때 사용해야 합니다. 스냅이 꺼져 있거나 이 요소에 적용되지 않아도 마찬가지입니다.
9.3. 2018년 8월 14일 CR 이후 변경 사항
2018년 8월 14일 후보 권고 이후의 변경 사항은 다음과 같습니다:
- scroll-padding 롱핸드에서 새로운 auto 키워드가 속성 정의 테이블에 포함되도록 수정했습니다. (이슈 3189)
- 속성 정의 테이블의 “계산된 값(Computed value)” 및 “애니메이션 타입(Animation type)” 줄을 수정했습니다.
- <percentage> 값이 scroll-margin 속성 정의 테이블에 잘못 들어간 부분을 정리했습니다. (3289)
의견 처리 결과(Disposition of Comments)를 확인할 수 있습니다.
9.4. 2017년 12월 14일 CR 이후 변경 사항
2017년 12월 14일 후보 권고 이후의 변경 사항은 다음과 같습니다:
- scroll-snap-align 축약형이 논리적 축약 규칙에 따라 block 축 값이 먼저, inline 축 값이 두 번째로 할당되도록 수정했습니다. (이슈 2232
- auto 키워드를 'scroll-padding'의 초기값으로 추가하여 UA 휴리스틱을 반영했습니다. (이슈 2728
-
scroll-snap-type 정의에서
scrollTo()
와 같은 프로그램적 스크롤도 스냅 대상임을 명확히 했습니다. (이슈 2593)관련 scroll-snap-type 속성을 스크롤 컨테이너에 적용하면, 저자는 스크롤 동작(스크롤 API 등 포함) 이후에 스크롤포트가 스냅 위치에 도달하도록 바이어스를 요청할 수 있습니다 (
scrollTo()
와 같은 프로그램적 스크롤 포함) . - § 5.2.1 유효한 스냅 위치의 범위
지정: 화면에 보이는 박스에 한정에서 문구를 더 명확히 수정했습니다—
이전 버전과 비교. (이슈 2526)
의견 처리 결과(Disposition of Comments)를 확인할 수 있습니다.
9.5. 2017년 8월 24일 CR 이후 변경 사항
2017년 8월 24일 후보 권고 이후의 변경 사항은 다음과 같습니다:
-
:target/
scrollIntoView()
/등은 스냅이 켜져 있든 꺼져 있든 scroll-margin을 반드시 고려해야 합니다. (이슈 1)페이지가 프래그먼트로 탐색되어 타겟 요소가 정의되면 (:target으로 매칭되거나,
scrollIntoView()
의 대상인 경우), UA는 해당 요소의 스크롤 스냅 영역을 사용해야 하며, 단순 border 박스만이 아니라, 스크롤 가능한 오버플로우 영역 중 어느 영역을 보여줄지 판단할 때 사용해야 합니다. -
:target/
scrollIntoView()
/등은 스냅이 켜져 있으면 반드시 스냅 위치를 사용해야 합니다. (이슈 1)페이지가 프래그먼트로 탐색되어 타겟 요소가 정의되고 (:target으로 매칭되거나,
scrollIntoView()
의 대상인 경우), 그 요소가 스냅 위치를 정의한다면, UA는shouldmust 스냅해야 하며, 그 요소의 스냅 위치 중 하나로 스냅해야 합니다 가장 가까운 스크롤 컨테이너가 스크롤 스냅 컨테이너일 경우 . UA는 스크롤 컨테이너에 scroll-snap-type: none이 설정된 경우에도 이를 수행할 수 있습니다. - scroll-snap-margin을 scroll-margin으로 이름을 변경하여, 스냅 동작과 관계없이 스크롤 요소에 여유 공간을 주는 더 일반적인 역할을 반영했습니다. (이슈 4)
의견 처리 결과(Disposition of Comments)를 확인할 수 있습니다.
9.6. 2016년 10월 20일 CR 이후 변경 사항
2016년 10월 20일 후보 권고 이후의 변경 사항은 다음과 같습니다:
-
scroll-padding 값을 음수가 아닌 값으로 제한했습니다.
(이슈 1084)
값은 음수가 아니어야 하며 padding과 동일하게 해석됩니다 …
- 페이징 및 홈 동작을 § 6.1 스크롤 방식의 종류 예시에 추가했습니다. (이슈 1605)
-
한 축에서의 스냅이 다른 축의 특정 스냅 영역에 스냅 가능 여부에 영향을 줄 수 있음을 명확히 했습니다.
(이슈 950)
scroll-snap-type: both는 스냅 위치를 각 축에서 독립적으로 평가하지만, 스냅 위치 선택 시 한 축에서의 스냅 위치가 다른 축의 스냅 위치에 영향을 줄 수 있습니다. 예를 들어, 한 축에서 스냅하면 다른 축이 정렬하려던 스냅 영역이 화면 밖으로 밀려나 해당 스냅 위치가 무효가 되어 선택할 수 없게 됩니다.
- scroll-padding 및 scroll-margin 축약형이 롱핸드에 값을 할당하는 방식에 대한 설명을 명확히 했습니다. (이슈 1050)
-
스크롤 스냅은 특정 입력 방식을 요구하지 않음을 명확히 했습니다.
(이슈 1305)
이 명세는 UA가 지원하는 스크롤 방식에만 적용됩니다; UA가 특정 입력이나 스크롤 방식을 반드시 지원해야 하는 것은 아닙니다.
- scroll-snap-stop이 다양한 스크롤 동작에 미치는 의도된 효과를 명확히 했습니다. (이슈 1552)
- scroll-snap-stop은 요소가 정의하는 스냅 위치에만 적용되고, 스크롤 스냅 컨테이너의 모든 스냅 위치에 적용되는 것은 아님을 명확히 했습니다.
- 예시의 구문 오류를 수정하고 scroll-snap-type 섹션에 새 예시를 추가했습니다. (이슈 827)
의견 처리 결과(Disposition of Comments)를 확인할 수 있습니다.