스크롤 상태 컨테이너 쿼리를 사용하면 요소가 '고정'되어 있는지(고정 위치 지정을 통해) 또는 '스냅'되어 있는지(스크롤 스냅을 통해)와 같은 현재 스크롤 상태에 따라 요소의 스타일을 지정할 수 있습니다. 이를 통해 JavaScript 교차 관찰자나 스크롤 이벤트 리스너를 사용하지 않고도 활성 항목을 시각적으로 구분할 수 있는 캐러셀 또는 갤러리 환경을 구현할 수 있습니다.
핵심 구현
스냅된 항목을 강조 표시하려면 스크롤 스냅 컨테이너를 설정하고, 스냅 타겟을 스크롤 상태 컨테이너로 정의한 다음, 해당 상태를 쿼리하여 하위 요소의 스타일을 지정해야 합니다.
1. 스크롤 스냅 컨테이너 설정
상위 컨테이너에 scroll-snap-type가 사용 설정되어 있어야 합니다.
<div class="carousel">
<div class="carousel-item">
<div class="card">Product 1 content</div>
</div>
<div class="carousel-item">
<div class="card">Product 2 content</div>
</div>
</div>
.carousel {
display: flex;
overflow-x: auto;
/* MANDATORY: Enable scroll snapping on the container */
scroll-snap-type: x mandatory;
}
2. 스냅 타겟을 스크롤 상태 컨테이너로 정의
스냅을 추적해야 하는 캐러셀의 각 항목은 scroll-state 컨테이너로 선언해야 합니다.
.carousel-item {
/* Define where the item snaps within the container */
scroll-snap-align: center;
/* MANDATORY: Establish this element as a scroll-state query container */
container-type: scroll-state;
}
3. snapped 상태 쿼리
컨테이너 쿼리는 하위 요소의 스타일을 지정하므로 스냅 타겟 내부의 요소에 강조 표시 스타일을 적용해야 합니다. 스크롤 컨테이너가 x축에서 오버플로되도록 설정되어 있으므로 scroll-state(snapped: x) 쿼리를 사용합니다.
필수: 동작 줄이기를 요청하지 않은 사용자에게만 효과를 표시하려면 스타일을 @media (prefers-reduced-motion: no-preference)로 래핑합니다. 사용 사례에 따라 효과의 일부를 유지할 수도 있지만, 이 경우 카드가 흰색에서 파란색으로 깜박여 일부 사용자에게 문제가 발생할 수 있으므로 완전히 사용 중지합니다.
/* Specify transition outside of queries so that it is applied regardless of state. */
.card {
transition:
scale 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
background-color 0.4s,
color 0.4s,
box-shadow 0.4s;
}
/*
Only show the effect for users not requesting reduced motion. Disable completely, including the color change, as it causes a flash that may be problematic.
*/
@media (prefers-reduced-motion: no-preference) {
/* Style the content when its parent .carousel-item is snapped on the x axis */
@container scroll-state(snapped: x) {
.card {
background: #007bff;
color: white;
scale: 1.15;
box-shadow: 0 10px 25px rgba(0, 123, 255, 0.3);
}
}
}
/* MANDATORY Copy-Paste Safety: Disable highlight scaling/flashing for motion sensitive users */
@media (prefers-reduced-motion: reduce) {
.card {
transition: none !important;
scale: 1 !important;
}
}
snapped 설명자는 특정 축(x, y, inline, block, both)을 쿼리할 수 있습니다.
접근성
AVOID: 상호작용 요소와 함께 scroll-state 사용
스냅된 항목의 시각적 하이라이트는 UX를 개선할 수 있지만 스냅된 항목은 접근성 트리에 노출되지 않습니다. 스냅된 항목에 적용된 시각적 테마는 요소가 활성 상태이거나 포커스가 있음을 전달해서는 안 되며, 키보드 포커스 링은 눈에 잘 띄고 snapped 강조표시와 구별되어야 합니다. 스냅된 항목이 대화형인 경우 다른 표준 접근성 관행을 사용하여 액세스할 수 있도록 해야 합니다.
스냅은 스크롤로 인해 발생하며, 키보드 포커스는 이동하지 않습니다. 하지만 키보드 포커스로 인해 스크롤 컨테이너가 이동하여 스냅된 항목이 변경될 수 있으며, 이 항목은 포커스가 지정된 항목일 수도 있고 아닐 수도 있습니다. 이는 사용자에게 혼란을 줄 수 있으므로 권장되지 않습니다.
대체 전략
스크롤 상태 쿼리를 지원하지 않는 브라우저의 경우 '활성' 강조 표시가 없어도 모든 항목을 읽을 수 있는 기능적 기본 환경을 제공해야 합니다.
특성 인식
@supports를 사용하여 지원되는 브라우저에만 개선사항을 제공할 수 있습니다.
@supports (container-type: scroll-state) {
/* Enhancement styles here */
}
자바스크립트 대체
강조표시가 사용자 환경에 중요한 경우 IntersectionObserver를 사용하여 맞춤 항목을 결정합니다. 음수 인라인 값이 있는 rootMargin를 제공하여 관찰 영역을 캐러셀 중앙의 얇은 슬라이스로 조정합니다. 예를 들어 캐러셀의 중앙 2% 에 있는 요소를 교차하는 것으로 간주하려면 rootMargin를 "0px -49%"로 설정합니다.
// Optional: detect support and apply a JS-based fallback
if (!CSS.supports('container-type', 'scroll-state')) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// Toggle a class based on intersection
entry.target.classList.toggle('is-snapped', entry.isIntersecting);
});
}, {
root: document.querySelector(".carousel"),
// Carousel item intersects if any part of the carousel item is in the middle 2% of the carousel.
rootMargin: "0px -49%"
});
document.querySelectorAll('.carousel-item').forEach(item => {
observer.observe(item);
});
}