스크롤 위치 인식 요소

개요

'맨 위로 이동' 링크와 같은 플로팅 버튼은 유용할 때만 표시하여 사용자 환경을 개선합니다. 이 가이드에서는 CSS container-scroll-state-queries를 사용하여 이러한 요소를 빌드하는 방법을 보여줍니다. 이를 통해 JavaScript 스크롤 리스너나 관찰자에 의존하지 않고 컨테이너의 스크롤 위치에 따라 요소의 스타일을 지정할 수 있습니다.

구현

1. 스크롤 컨테이너 설정

스크롤 컨테이너는 스크롤 상태 쿼리 컨테이너로 선언해야 합니다.

.scroller {
  overflow-y: auto;
  /* Establish this element as a scroll-state query container */
  container-type: scroll-state;
}

2. 플로팅 요소 스타일 지정

컨테이너 내부에 요소를 배치하고 스타일을 지정합니다. 기본적으로 숨겨져 있어야 합니다.

.back-to-top {
  position: sticky;
  bottom: 20px;
  visibility: hidden;
  opacity: 0;
  translate: 0 20px;
  transition:
    visibility 0.3s,
    opacity 0.3s ease,
    translate 0.3s ease;
}

3. 스크롤 상태에 응답

scroll-state 함수와 함께 @container 규칙을 사용합니다. 사용자가 아래로 스크롤했는지 확인하려면 컨테이너를 상단으로 스크롤할 수 있는지 확인합니다.

/* When the container can be scrolled toward the top, it means the user has scrolled down */
@container scroll-state(scrollable: top) {
  .back-to-top {
    visibility: visible;
    opacity: 1;
    translate: 0 0;
  }
}

대체 전략

기본 대체

container-scroll-state-queries가 지원되지 않으면 기본 visibility: hidden로 인해 플로팅 요소가 표시되지 않습니다. 기능을 보장하기 위해 지원되지 않는 브라우저에서 요소를 항상 표시하도록 선택할 수 있습니다.

/* Fallback for browsers that do not support the feature */
.back-to-top {
  visibility: visible; /* Always visible */
  opacity: 1;
}

/* Override for supported browsers to handle dynamic visibility */
@supports (container-type: scroll-state) {
  .back-to-top {
    visibility: hidden;
    opacity: 0;
  }
  
  @container scroll-state(scrollable: top) {
    .back-to-top {
      visibility: visible;
      opacity: 1;
      translate: 0 0;
    }
  }
}

고급 대체 (Intersection Observer)

동적 가시성이 필요한 경우 IntersectionObserver를 사용하여 스크롤러 상단의 센티널 요소가 보이지 않을 때 클래스를 전환합니다.

<!-- Sentinel element placed at the top of the scroller -->
<div class="scroll-sentinel"></div>
/* Marker styling to ensure it does not affect layout */
.scroll-sentinel {
  height: 0;
  width: 0;
  visibility: hidden;
}

.scrolled .back-to-top {
  visibility: visible;
  opacity: 1;
  translate: 0 0;
}
if (!CSS.supports('container-type', 'scroll-state')) {
  const sentinel = document.querySelector('.scroll-sentinel');
  const scroller = document.querySelector('.scroller');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      // If the sentinel is NOT intersecting, it means the user has scrolled down
      if (!entry.isIntersecting) {
        scroller.classList.add('scrolled');
      } else {
        scroller.classList.remove('scrolled');
      }
    });
  }, { root: scroller });

  observer.observe(sentinel);
}