GODRICH

CSS 무한 롤링 배너 9종 — AOS·GSAP 없이, JS 0줄

css 무한 롤링 배너는 로고나 문구를 담은 콘텐츠 줄이 끊김 없이 한 방향으로 계속 흘러가는 애니메이션입니다. AOS·GSAP 없이, JS 0줄로 아홉 가지를 전부 만들었고, 그중 하나(09)는 화면이 아니라 사용자의 운영체제 설정을 실제로 반영합니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 기법이 쌓이는 순서로 놓았습니다. 이음매 없이 도는 기본 공식(01)을 실전 로고 배치(02)에 옮기고, 상호작용(03)과 시각 다듬기(04)를 얹은 뒤 세로·양방향 배치(05~06)로 변형하고, 속도와 입체감(07~08)으로 마무리해 접근성(09)으로 끝냅니다. 데모는 실제 서비스보다 배속했습니다 — 실제로는 12~20초에 한 바퀴가 보통이고, 로고·문구 자리도 실서비스에 맞는 것으로 바꾸면 됩니다.

01끊김 없는 루프 공식

칩 A부터 F까지를 그대로 한 번 더 복제해 트랙 하나에 열두 개를 나란히 두고, transform 을 0에서 -50%까지만 옮기는 게 이 아래 모든 마퀴가 쓰는 기본 공식입니다. 간격은 트랙의 gap 이 아니라 칩마다 margin-right 로 줍니다. 그래야 한 세트의 폭이 트랙의 정확히 절반이 되어, 복제본이 원본이 있던 자리에 픽셀 단위로 겹쳐 듭니다.

translateX(-50%)콘텐츠 2벌@keyframes
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 12s linear infinite;
}
.marquee__chip { margin-right: 14px; }   // 트랙 gap 대신 — 세트 폭이 딱 절반
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

02로고 무한 롤링

특정 서비스가 아니라 실제로 쓸 배치를 보여주려고, 대문자 워드마크 다섯 개를 알약 카드로 만들어 같은 이음매 없는 트랙에 흘려보냅니다. 파트너사 라벨을 스트립 위에 작게 얹어 이 목록이 무엇을 나열한 것인지 알려줍니다.

로고 카드gap 고정폭linear infinite
.marquee__track {
  display: flex;
  animation: logo-scroll 12s linear infinite;
}
.marquee__logo {
  margin-right: 16px;
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  background: #fff;
}

03호버 시 정지

마우스가 배너 위에 올라오면 animation-play-state 를 paused 로 바꿔 흐름을 멈추고, 읽던 문구를 그 자리에 그대로 둡니다. 렌더는 실제 마우스를 못 움직이니, 데모에서는 트랙 자신의 keyframes 안에 정지 구간을 넣어 흐르다 멈췄다 다시 흐르는 모습을 자동으로 반복시켰습니다.

animation-play-statepausedhover
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}
@keyframes marquee-hover-cycle {
  0%   { transform: translateX(0); }
  38%  { transform: translateX(-32%); }
  62%  { transform: translateX(-32%); }
  100% { transform: translateX(-50%); }
}

04양끝 마스크 페이드

트랙을 감싼 상자에 mask-image 그러데이션을 씌워 좌우 끝 요소가 뚝 잘리는 대신 배경 속으로 옅어지며 사라지게 만들었습니다. 접두사 없는 mask-image 는 Safari 15.4부터 지원하지만(caniuse 의 CSS Masks 지원표), 그 이전 버전과도 같은 결과를 내려면 -webkit-mask-image 를 나란히 남겨 두는 게 안전합니다.

mask-imagelinear-gradient-webkit-mask
.marquee {
  overflow: hidden;
  mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
}

05세로 티커

translateX 대신 translateY 를 써서 같은 2벌 복제 공식을 세로 축에 그대로 적용했습니다. 종목명·가격·등락률을 한 행에 담아 고정 높이 상자 안에서 위로 흘러가게 했습니다.

translateY(-50%)column 트랙고정 높이
.ticker__track {
  display: flex;
  flex-direction: column;
  animation: ticker-scroll 12s linear infinite;
}
@keyframes ticker-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

06양방향 흐름

위아래 두 트랙에 같은 marquee-scroll 애니메이션을 그대로 걸고, 아래 트랙에만 animation-direction: reverse 를 얹어 방향만 뒤집었습니다. 새 keyframes 를 따로 만들 필요가 없어 코드가 절반으로 줄었습니다.

animation-direction: reverse두 트랙교차 흐름
.marquee__track {
  animation-name: marquee-scroll;
  animation-duration: 12s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.marquee__row--rev .marquee__track {
  animation-direction: reverse;
}

07속도 완급 흐름

같은 -50% 이동인데 타이밍 함수만 linear 대신 cubic-bezier(.65,0,.35,1) 로 바꿨습니다. 이음매 근처는 느리게 붙고 구간 중간은 빠르게 지나가는 리듬이 생겨, 컨베이어 벨트 같던 인상이 한결 부드러워집니다.

cubic-bezier비선형 easing이음매 유지
.marquee__track {
  animation: marquee-scroll 9s
    cubic-bezier(.65, 0, .35, 1) infinite;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

083D 곡선 마퀴

무대에 perspective, 트랙에 preserve-3d 를 걸고 카드 다섯 장에 -44도부터 44도까지 다섯 단계 rotateY 와 translateZ 를 nth-child 로 나눠 줬습니다. 복제본(6~10번째)에도 같은 각도표를 반복해서 이음매에서 곡률이 끊기지 않게 했습니다.

perspectivepreserve-3d단계별 rotateY
.stage { perspective: 420px; }
.marquee__track { transform-style: preserve-3d; }
.marquee__card:nth-child(1),
.marquee__card:nth-child(6) {
  transform: rotateY(-44deg) translateZ(-40px);
}
.marquee__card:nth-child(3),
.marquee__card:nth-child(8) {
  transform: rotateY(0deg) translateZ(0px);
}

09정지 대체판

동작 줄이기 토글이 켜지는 순간 트랙 애니메이션도 같은 keyframes 안에서 그 지점에 멈춰, 실제 prefers-reduced-motion: reduce 사용자가 보는 화면과 같은 정지 상태를 미리 보여줍니다. 상태 글자도 "재생 중"에서 "정지됨"으로 함께 바뀝니다.

prefers-reduced-motion정적 목록토글 비교
@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none !important;
    transform: translateX(-30%);
  }
  .panel__toggle-knob {
    transform: translateX(16px);
  }
}

어디서 깨지는가 — 함정 둘

02 로고 스트립을 처음 만들 때는 복제본 다섯 장에 aria-hidden 을 깜빡했습니다. 스크린리더로 확인하니 NORTH·ACME·ORBIT·VELA·KADE 다섯 개가 두 번씩 그대로 읽혀, 파트너사가 실제로는 열 곳인 것처럼 들렸습니다. MDN 의 aria-hidden 문서는 이 속성이 보조 기술에서만 요소를 감추고 화면에서는 그대로 보이게 한다고 정리해 둡니다. 복제본 쪽 span 마다 aria-hidden="true" 를 달자 안내 음성이 다섯 개만 읽고 끝났습니다.

처음엔 아홉 개 전부 트랙에 flex 의 gap 을 걸었습니다. 그런데 칩 열두 개 사이에 gap 은 열한 번만 생기므로 -50% 는 gap 의 절반만큼 모자라고, 한 바퀴마다 그만큼 튑니다. 크롬에서 트랙 절반 지점과 -50% 도착점의 차를 재 보니 01 이 7px, 03 이 20px, 07 이 11px 로 아홉 개 중 여덟 개가 어긋나 있었습니다. gap 을 지우고 칩마다 margin-right 를 준 뒤 다시 재니 전부 0.3px 이내로 들어왔습니다. gap 을 꼭 써야 한다면 도착점을 translateX(calc(-50% - 7px)) 처럼 gap 의 절반만큼 더 옮기면 되지만, 값 두 곳을 맞춰야 해서 margin 쪽이 덜 틀립니다. MDN 의 gap 문서대로 gap 은 항목 사이에만 생기고 끝에는 생기지 않는다는 점이 핵심입니다.

이 아홉 개 파일을 전부 렌더해 이음매까지 확인한 압축 파일의 비밀번호는 gcjydr57이고, 아래 버튼으로 받을 수 있습니다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce 에서 애니메이션을 끄고 마지막 위치에 고정해 둡니다. 03 은 hover·focus-within 에서도 같은 정지가 걸려 키보드 사용자가 Tab 으로 배너에 들어가도 문구를 놓치지 않습니다. 09 는 한 단계 더 나가, 실제로 그 설정을 켰을 때 화면이 어떻게 바뀌는지를 토글로 미리 보여줘 눈으로 차이를 확인할 수 있게 했습니다. 세로 티커(05)도 예외 없이 같은 규칙을 따라 정지 시 목록이 흐르지 않고 그대로 멈춥니다. MDN 의 prefers-reduced-motion 문서에 이 미디어 특성의 브라우저 지원과 값 두 가지가 정리돼 있습니다.

이 아홉 개 말고 다른 스크롤 애니메이션이 궁금하면 CSS 카테고리 페이지를 열어 보시고, 사이트 소개는 여기에서 확인하십시오.

FAQ

무한 롤링 배너에 실제 로고 이미지를 꼭 써야 합니까?

아닙니다. 이 글의 데모는 로고 자리에 짧은 대문자 워드마크만 넣어 이음매와 속도에 집중하게 했습니다. 실제로 쓸 때는 marquee__logo 안 텍스트를 <img> 태그로 바꾸고 높이를 34px 안팎으로 맞추면 같은 트랙 코드가 그대로 동작합니다.

아홉 개를 한 페이지에 동시에 띄워도 성능에 문제없습니까?

대부분은 transform 만 바뀌어 레이아웃을 다시 계산하지 않지만, 04 의 mask-image 와 08 의 preserve-3d 는 합성 레이어를 추가로 쓰는 만큼 화면에 마퀴가 여러 개 겹칠 때는 이 둘부터 개수를 줄이는 게 낫습니다. will-change 를 아홉 개 트랙 전부에 미리 걸어 둘 필요는 없습니다. 애니메이션이 걸린 요소는 브라우저가 알아서 최적화하므로, 레이어를 실제로 더 쓰는 04·08 처럼 필요한 곳에만 붙이는 편이 안전합니다.

마우스가 없는 터치 화면에서는 03 이 어떻게 동작합니까?

호버가 없는 기기에서는 :hover 규칙이 걸리지 않으니 트랙이 계속 흐릅니다. 배너를 탭해 포커스를 주면 :focus-within 규칙이 대신 걸려 흐름이 멈추므로, 손가락으로 짚어 읽는 동안은 문구가 고정됩니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.