sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션

7 min read
CSSsibling-indexscroll-driven animationsscrollytelling
sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션

수백 개의 글자가 스크롤에 따라 소용돌이치며 화면에 나타나는 효과. 이런 걸 보면 본능적으로 "JavaScript 무거운 거 쓰겠지"라고 생각한다. 그런데 CSS만으로 된다면? CSS-Tricks에 올라온 이 글은 sibling-index()animation-timeline: scroll()을 결합해서 순수 CSS 스크롤 소용돌이를 구현한다.

sibling-index()가 뭔가

sibling-index()는 형제 요소들 사이에서 자신의 순서 번호를 반환하는 CSS 함수다. 비유하면 줄을 서 있는 사람이 "나는 앞에서 몇 번째야?"를 CSS에서 직접 물어볼 수 있게 된 것이다.

기존에는 :nth-child()로 "n번째 요소에 이 스타일을 적용해라"라고 지시할 수 있었지만, n 값 자체를 계산에 사용하는 건 불가능했다. sibling-index()는 이 제약을 깬다.

sibling-index-기본.css
.item {
  /* 각 요소의 순서에 비례해서 투명도 감소 */
  opacity: calc(1 - (sibling-index() / sibling-count()));
}

[💡 잠깐! 이 용어는?] sibling-count(): 같은 부모 아래 형제 요소의 총 개수를 반환하는 CSS 함수다. sibling-index()와 함께 사용하면 "전체 중 몇 번째인지"의 비율을 계산할 수 있다.


소용돌이의 수학

스크롤 보텍스 효과의 핵심은 각 글자에 회전 각도, 중심으로부터의 거리, 크기를 순서에 따라 점진적으로 적용하는 것이다. 첫 번째 글자는 바깥쪽에서 크게, 마지막 글자는 안쪽에서 작게.

공식은 이렇다:

속성값 = 시작값 - ((감소량 / 총_글자수) × 글자_인덱스)

이걸 CSS 변수와 sibling-index()로 표현하면:

spiral-variables.css
.char {
  --radius: calc(10vh - (7vh / sibling-count() * sibling-index()));
  --rotation: calc((360deg * 3 / sibling-count()) * sibling-index());
  --scale: calc(0.4 - (0.25 / sibling-count() * sibling-index()));
}

각 변수의 역할을 풀어보면:

변수역할동작
--radius중심으로부터의 거리첫 글자 10vh → 마지막 글자 3vh
--rotation회전 각도총 3바퀴(1080°)를 글자 수로 분배
--scale크기 비율0.4에서 점점 작아짐

비유하면 소라 껍데기의 나선 구조다. 바깥에서 시작해서 안쪽으로 갈수록 간격이 좁아지고 크기가 줄어든다.


스크롤 애니메이션 연결

여기까지는 정적인 소용돌이 배치다. 여기에 animation-timeline: scroll()을 연결하면 스크롤에 따라 글자가 하나씩 나타나는 효과가 만들어진다.

scroll-driven-spiral.css
.char {
  --radius: calc(10vh - (7vh / sibling-count() * sibling-index()));
  --rotation: calc((360deg * 3 / sibling-count()) * sibling-index());
  --scale: calc(0.4 - (0.25 / sibling-count() * sibling-index()));
 
  position: absolute;
  transform:
    rotate(var(--rotation))
    translateY(calc(-2.9 * var(--radius)))
    scale(var(--scale));
 
  animation: fade-in linear both;
  animation-timeline: scroll();
  animation-range: calc(sibling-index() / sibling-count() * 100%) calc((sibling-index() + 1) / sibling-count() * 100%);
}
 
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

핵심은 animation-range다. 각 글자의 페이드인 시점이 자신의 인덱스 비율에 따라 달라지므로, 스크롤하면 순서대로 하나씩 나타난다. JavaScript 타이밍 로직이 전혀 필요 없다.

[💡 잠깐! 이 용어는?] animation-timeline: scroll(): CSS 애니메이션의 진행을 시간이 아닌 스크롤 위치에 연동하는 선언이다. 스크롤 0%에서 시작, 100%에서 완료. 메인 스레드를 점유하지 않아서 JavaScript 방식보다 성능이 좋다.


JavaScript 방식과 비교

이 소용돌이 효과의 원조는 JavaScript 기반 텍스트 보텍스 라이브러리다. 원작자조차 "글자 하나하나에 개별 애니메이션을 거는 건 천문학적인 성능 비용이 든다"고 경고했다. JavaScript로 수백 개의 DOM 요소에 매 프레임 스타일을 계산하면 당연히 무겁다.

방식장점단점
JavaScript브라우저 호환성 완벽메인 스레드 점유, 모바일 성능 저하
CSS sibling-index()메인 스레드 미점유, 선언적Firefox 미지원, 텍스트 분할은 JS 필요

CSS 방식에도 한 가지 한계가 있다. 텍스트를 글자 단위 <span>으로 분할하는 단계는 여전히 JavaScript(GSAP의 SplitText 등)가 필요하다. CSS 자체로는 문자열을 개별 요소로 쪼갤 수 없기 때문이다. 하지만 애니메이션 자체는 100% CSS이므로 렌더링 성능은 확실히 이점이 있다.


브라우저 지원

기능ChromeSafariFirefox
sibling-index()지원지원미지원
sibling-count()지원지원미지원
animation-timeline: scroll()지원지원지원

Firefox에서 sibling-index()가 아직 빠져 있다. 프로덕션에 바로 적용하기는 어렵고, 프로토타입이나 데모 사이트에서 먼저 활용해볼 만하다. Interop 2026 집중 영역에는 포함되지 않았지만, Scroll-driven Animations는 포함되어 있으므로 스크롤 애니메이션 자체의 호환성은 빠르게 개선될 것이다.

정리

  • sibling-index()는 형제 요소의 순서 번호를 CSS 계산에 직접 사용할 수 있게 해주는 함수다
  • scroll() 타임라인과 결합하면 JavaScript 없이 스크롤 기반 순차 애니메이션을 구현할 수 있다
  • 수백 개 요소의 개별 애니메이션도 CSS 컴포지터 스레드에서 처리되므로 성능 이점이 크다
  • Firefox 미지원이 유일한 걸림돌이다. 프로토타입 용도로 먼저 경험을 쌓아두는 게 좋다

참고:

관심 있을 만한 포스트

CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간

Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.

CSSbase-select

CSS @property — 커스텀 속성에 타입을 부여하는 방법

CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.

CSS@property

CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스

CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.

CSS:near()

Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다

Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.

InteropCSS

CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까

CSS 쌓임 맥락의 생성 조건, z-index의 실제 작동 원리, 그리고 레이아웃 버그를 디버깅하는 실전 전략을 정리한다.

CSSz-index

OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기

conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.

CSSconic-gradient

배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.

CSScontrast-color

포크레인 없이 못 박기 — CSS 수학 함수만으로 바 차트 그리기

CSS 수학 함수와 커스텀 프로퍼티, 시맨틱 HTML을 조합해 JavaScript 없이 반응형 바 차트를 구현하는 방법을 다룬다.

CSScalc()