Scroll Snap 속성

김종민·2025년 7월 24일
0

html / css

목록 보기
29/29
post-thumbnail

fullPage.js 처럼 섹션별로 페이지가 자동으로 넘어가는 기능이 없을까 찾아보다가
css로도 가능하다는걸 찾게 되어서 아카이빙을 진행하려 한다!
그건 바로 CSS의 Scroll snap이다!!


CSS Scroll snap

CSS Scroll snap은 웹 페이지에서 스크롤을 할 때, 요소가 스크롤되는 위치에 자동으로 스냅되도록 하는 CSS 속성이다.
예를들어 사용자가 웹 페이지를 스크롤할 때 중간에 멈춰버리면 콘텐츠의 중간에서 멈춰 주요 콘텐츠의 일부만 보이게 되는데, 이를 미리 설정한 위치로 자동 스냅하여 자연스러운 스크롤 움직임과 함께 사용자 경험을 더욱 향상시키게 된다!

스크롤 스냅을 적용하면 절반만 스크롤해도 해당 영역이 완전히 화면에 스냅된다

스크롤 스냅이 없던 시절에는 자바스크립트로 일일히 요소 위치를 계산해서 조정해야 됐지만, CSS 속성을 통해 공식 지원함으로써 개발의 편리함과 더불어 성능도 비약적으로 상승했다고 볼 수 있다.


Scroll snap 사용법

우선 스크롤 스냅을 적용할 부모 컨테이너 영역을 만들고, 그 안에 스냅될 자식 요소 영역을 만들어 준다. 그리고 부모에 scroll-snap-type 속성자식에 scroll-snap-align 속성을 적용해주면 된다. (이들 속성에 대해서는 아래 자세히 다룬다)

<html>
  <div class="scroll-container">
      <div class="scroll-area">1</div>
      <div class="scroll-area">2</div>
      <div class="scroll-area">3</div>
      <div class="scroll-area">4</div>
  </div>
  
  
  <style>
    /* 부모 스크롤 스냅 컨테이너 */
    .scroll-container {
      overflow: auto;
      scroll-snap-type: y mandatory; /* y 축 방향으로만 scroll snap 적용 */
    }

    /* 자식 스크롤 스냅 영역 */
    .scroll-area {
      scroll-snap-align: start; /* 스크롤 위치 맞춤 */
    }


    /* 여기서부턴 단순 꾸미기 --------------------------- */
    .scroll-area:nth-of-type(1) {
      background: #49b293;
    }

    .scroll-area:nth-of-type(2) {
      background: #c94e4b;
    }

    .scroll-area:nth-of-type(3) {
      background: #4cc1be;
    }

    .scroll-area:nth-of-type(4) {
      background: #8360A6;
    }
  </style>
</html>

Scroll snap 속성

이제 Scroll snap의 속성에 대해서 알아보자!

1. scroll-behavior: smooth;

위의 시안을 보면 스크롤이 휙휙하고 빠르게 넘어가는데, 이 속도를 scroll-behavior: smooth로 조절이 가능하다!

2. scroll-snap-type

scroll-snap-type 속성은 스크롤 스냅이 동작하는 방식을 지정한다. 첫 번째 인자로 스냅이 적용될 축을 지정하고, 두 번째 인자로 스냅 적용 방식을 지정한다.

<style>
  .container {
      scroll-snap-type : [scroll snap axis], [scroll snap strictness];
  }

  /* y 방향으로 스크롤 스냅을 적용 */
  .container {
    scroll-snap-type: y mandatory;
  }

  /* x 방향으로 스크롤 스냅을 적용 */
  .container {
    scroll-snap-type: x proximity;
  }
</style>

scroll snap axis

  • x: 수평(가로) 축으로 snap position 지정
  • y: 수직(세로) 축으로 snap position 지정
  • block: snap area의 block 축으로 지정
  • inline: inline 축으로 지정
  • both: 두 축의 위치를 개별적으로 스냅.

scroll snap strictness

  • none: 스냅하지 않음.
  • proximity: 스크롤 위치가 스냅 위치에 가까워지면 자연스럽게 스냅
  • mandatory: 스크롤 위치가 스냅 위치와 정확히 일치해야만 스냅

💡 scroll-snap-type 속성 값을 mandatory 로 선언 시 주의할점은, 콘텐츠 간의 간격이 넓을 때 강제로 스냅을 하게 되면 중간 콘텐츠를 건너뛰고 다음 콘텐츠로 이동하는 경우가 발생할 수 있다는 점이다.


3. scroll-snap-align

scroll-snap-align 속성은 스크롤 스냅이 적용되는 요소가 스냅 위치에 정렬되는 방식을 지정한다. scroll-snap-type 에서 지정한 축을 기준으로 snap area의 정렬을 정한다.

  • none: snap position을 지정하지 않음
  • start: 스크롤 스냅 위치를 snap area 시작 부분에 맞추도록 지정
  • end: 스크롤 스냅 위치를 snap area의 끝 부분에 맞추도록 지정
  • center: 스크롤 스냅 위치를 snap area의 가운데 부분에 맞추도록 지정
<style>
  /* 스냅 위치에 맞춰 요소 시작 부분을 정렬 */
  .item {
    scroll-snap-align: start;
  }

  /* 스냅 위치에 맞춰 요소 중앙 부분을 정렬 */
  .item {
    scroll-snap-align: center;
  }

  /* 스냅 위치에 맞춰 요소 끝 부분을 정렬 */
  .item {
    scroll-snap-align: end;
  }
</style>


4. scroll-padding / scroll-margin

scroll-padding, scroll-margin속성은 스크롤 영역의 패딩과 마진을 조정하는 속성이다. 이 속성을 사용하면 스크롤 스냅이 적용되는 영역을 확보할 수 있다. 
정말로 요소의 padding, margin 값이 변경되는 것이 아니고, 해당 뷰 포트(viewport)의 가짜 padding, margin이 적용되는 것이다.

<style>
  /* 스크롤 영역 위/아래로 패딩 100px 추가 */
  .container {
    scroll-snap-type: y mandatory; 
    scroll-padding: 100px 0 100px 0; /* `scroll-padding-top`, `scroll-padding-right`, `scroll-padding-bottom`, `scroll-padding-left`  */
  }

  /* 스크롤 영역 상/하/좌/우 모두에 패딩 50px 추가 */
  .container {
    scroll-snap-type: y mandatory; 
    scroll-padding: 50px;
  }
  
  
  
  
  /* 스크롤 스냅이 적용되는 영역의 상/하/좌/우 마진을 100px로 지정 */
  .container {
    scroll-margin: 100px; /*  `scroll-margin-top`, `scroll-margin-right`, `scroll-margin-bottom`, `scroll-margin-left` */
  }

  /* 스크롤 스냅이 적용되는 영역의 상/하 마진을 50px, 좌/우 마진을 100px로 지정 */
  .container {
    scroll-margin: 50px 100px;
  }

</style>


5. scroll-snap-stop

scroll-snap-stop 속성은 스크롤 스냅 위치에 도달했을 때 스크롤을 중지할 지 여부를 지정한다.

  • normal: 스크롤이 스냅되더라도 계속 진행될 수 있음
  • always: 스크롤이 스냅 위치에 도달하면 중지
<style>
  /* 스크롤 스냅이 요소의 끝에서 중단되도록 지정 */
  .item {
    scroll-snap-stop: always;
  }

  /* 스크롤 스냅이 요소의 중앙에서만 중단되도록 지정 */
  .item {
    scroll-snap-stop: normal;
  }
</style>



📝 나중에 재활용 하기 위한 코드 기록

virtically 페이지 샘플

<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Slow Scroll Snap Example</title>
    <style>
      /* 기본 HTML 스타일 */
      html {
        scroll-behavior: smooth; /* 부드러운 스크롤 기본 설정 */
      }

      /* 컨테이너 설정 */
      .container {
        height: 100vh;
        overflow-y: scroll;
        scroll-snap-type: y mandatory; /* y축 스냅, mandatory로 강제 스냅 */
      }

      /* 섹션 스타일 */
      section {
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 2rem;
        scroll-snap-align: start; /* 섹션 시작점에 스냅 */
      }

      /* 섹션별 색상 */
      .section1 { background-color: #f0f0f0; }
      .section2 { background-color: #d0e8ff; }
      .section3 { background-color: #ffd0d0; }
    </style>
  </head>
  <body>
    <div class="container">
      <section class="section1">섹션 1</section>
      <section class="section2">섹션 2</section>
      <section class="section3">섹션 3</section>
    </div>

    <script>
      // JavaScript로 스크롤 속도 제어
      const container = document.querySelector('.container');
      let isScrolling;

      container.addEventListener('wheel', (event) => {
        event.preventDefault(); // 기본 스크롤 동작 방지

        clearTimeout(isScrolling);

        // 스크롤 방향 감지
        const delta = event.deltaY > 0 ? 1 : -1;
        const sections = document.querySelectorAll('section');
        let currentSection = 0;

        // 현재 스크롤 위치에 따라 섹션 인덱스 계산
        sections.forEach((section, index) => {
          if (container.scrollTop >= section.offsetTop - 50) {
            currentSection = index;
          }
        });

        // 다음 또는 이전 섹션으로 이동
        const nextSection = Math.min(Math.max(currentSection + delta, 0), sections.length - 1);
        const targetTop = sections[nextSection].offsetTop;

        // 부드럽고 느린 스크롤 애니메이션
        container.scrollTo({
          top: targetTop,
          behavior: 'smooth'
        });

        // 스크롤이 완료될 때까지 추가 스크롤 방지
        isScrolling = setTimeout(() => {
          isScrolling = null;
        }, 1000); // 1초 동안 추가 스크롤 방지 (속도 조절)
      });
    </script>
  </body>
</html>

horizon 페이지 샘플

<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Horizontal Scroll Snap with Center Alignment (Fixed)</title>
    <style>
      html {
        scroll-behavior: smooth; /* 부드러운 스크롤 기본 설정 */
      }

      body {
        margin: 0;
        overflow: hidden; /* 불필요한 스크롤바 방지 */
      }

      .container {
        width: 100vw;
        height: 100vh;
        overflow-x: auto; /* 가로 스크롤 활성화 */
        overflow-y: hidden; /* 세로 스크롤 비활성화 */
        display: flex; /* 섹션을 가로로 배치 */
        scroll-snap-type: x proximity; /* x축 스냅, proximity로 자연스러운 스냅 */
        white-space: nowrap; /* 섹션이 줄바꿈되지 않도록 */
        -webkit-overflow-scrolling: touch; /* 모바일에서 부드러운 스크롤 */
      }

      section {
        width: 100vw; /* 각 섹션이 뷰포트 너비와 동일 */
        height: 100vh;
        display: inline-flex; /* 가로로 정렬 */
        justify-content: center;
        align-items: center;
        font-size: 2rem;
        scroll-snap-align: center; /* 섹션의 중앙에 스냅 */
        flex-shrink: 0; /* 섹션 크기 고정 */
      }

      .section1 { background-color: #f0f0f0; }
      .section2 { background-color: #d0e8ff; }
      .section3 { background-color: #ffd0d0; }
    </style>
  </head>
  <body>
    <div class="container">
      <section class="section1">섹션 1</section>
      <section class="section2">섹션 2</section>
      <section class="section3">섹션 3</section>
    </div>

    <script>
      const container = document.querySelector('.container');
      let isScrolling = false;

      container.addEventListener('wheel', (event) => {
        event.preventDefault(); // 기본 스크롤 동작 방지

        if (isScrolling) return;

        // 수직 휠을 가로 스크롤로 변환
        const delta = event.deltaY > 0 ? 1 : -1;
        const sections = document.querySelectorAll('section');
        let currentSection = 0;

        // 현재 스크롤 위치에 따라 섹션 인덱스 계산
        sections.forEach((section, index) => {
          const sectionLeft = section.offsetLeft;
          const sectionWidth = section.offsetWidth;
          const sectionCenter = sectionLeft + sectionWidth / 2;

          if (container.scrollLeft + window.innerWidth / 2 >= sectionCenter - 50 &&
              container.scrollLeft + window.innerWidth / 2 <= sectionCenter + 50) {
            currentSection = index;
          }
        });

        // 다음 또는 이전 섹션으로 이동
        const nextSection = Math.min(Math.max(currentSection + delta, 0), sections.length - 1);
        const targetLeft = sections[nextSection].offsetLeft + (sections[nextSection].offsetWidth - window.innerWidth) / 2;

        isScrolling = true;
        container.scrollTo({
          left: targetLeft,
          behavior: 'smooth' // 부드러운 스크롤
        });

        // 스크롤 완료 후 isScrolling 플래그 해제
        setTimeout(() => {
          isScrolling = false;
        }, 1000); // 1초 딜레이로 스크롤 간섭 방지
      });
    </script>
  </body>
</html>
profile
웹 퍼블리셔의 코딩 일기

0개의 댓글