GODRICH

CSS 히어로 섹션 애니메이션 9종

히어로 섹션은 사이트에 들어가자마자 가장 먼저 보이는 큰 화면 하나, 큰 제목과 버튼이 자리 잡은 그 첫인상을 말한다. AOS·GSAP 없이, JS 0줄로 그 화면에 움직임을 주는 CSS 애니메이션 9가지를 모았다.

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

아홉 개는 인기순이 아니라 방문자가 화면을 여는 순간부터 겪는 순서로 놓았다. 열자마자 시선이 가는 헤드라인(01)에서 시작해 배경의 은은한 움직임(02)·시선이 멈추는 CTA 버튼(03)을 지나, 제품을 보여주는 목업(04)과 눈길을 끄는 배지(05)로 신뢰를 더한다. 그 아래로 스크롤하면 지나가는 파트너사 스트립(06)까지가 로드 직후의 순서고, 나머지 셋(07~09)은 사용자가 실제로 스크롤하거나 뭔가를 저장할 때 반응한다.

01헤드라인 순차 등장

헤드라인 세 줄이 한꺼번에 뜨지 않고 한 줄씩 늦게 등장한다. 줄마다 따로 @keyframes 를 두고 시작 지점을 10%·20%·30% 로 어긋나게 배치해, opacitytranslateY 두 값만으로 순서를 만든다.

@keyframesopacity · translateYstagger delay
.hero__line { opacity: 0; transform: translateY(24px); }

.hero__line--1 { animation: line-in-1 $duration $easing infinite; } // $easing: $ease-spring

@keyframes line-in-1 {
  0%   { opacity: 0; transform: translateY(24px); }
  10%  { opacity: 1; transform: translateY(0); }
  85%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(24px); }
}

02배경 그라디언트 드리프트

카드보다 훨씬 큰 conic-gradient 층을 ::before 에 깔고 overflow: hidden 으로 가둔 뒤, translaterotate 만으로 천천히 돌린다. background-position 을 움직이는 방식과 달리 카드 크기가 바뀌어도 그라디언트가 다시 계산되지 않는다.

::beforeconic-gradienttranslate · rotate
.hero::before {
  content: '';
  position: absolute;
  inset: -60% -60%;
  background: conic-gradient(from 0deg, #ff477e, #7b2ff7, #4cc9f0, #ff477e);
  animation: drift $duration $easing infinite; // $easing: $ease-out
}
@keyframes drift {
  0%   { transform: translate(-6%, -4%) rotate(0deg); }
  50%  { transform: translate(6%, 5%) rotate(180deg); }
  100% { transform: translate(-6%, -4%) rotate(360deg); }
}

03CTA 펄스 글로우

버튼이 scalebox-shadow 를 같이 오가며 숨 쉬듯 커졌다 작아진다. 데모용 반복 재생과 별개로 실제 :hover·:focus-visible 에서도 같은 색으로 반응해, 루프를 꺼도 버튼 자체는 그대로 살아 있다.

box-shadowscale:hover · :focus-visible
.cta:hover { transform: translateY(-2px); }

.cta { animation: pulse $duration $easing infinite; } // $easing: $ease-pop

@keyframes pulse {
  0%   { transform: scale(1);     box-shadow: 0 0 0 0 rgba(255, 224, 102, .55); }
  50%  { transform: scale(1.045); box-shadow: 0 0 32px 8px rgba(255, 224, 102, .55); }
  100% { transform: scale(1);     box-shadow: 0 0 0 0 rgba(255, 224, 102, 0); }
}

04목업 플로트

브라우저 창처럼 상단 바가 달린 목업 카드 전체가 translateY 와 아주 작은 rotate 를 오가며 둥둥 떠 있는 것처럼 보인다. 두 값 다 이동(transform)만 건드려서 레이아웃이 다시 계산될 일이 없다.

transform: translateYrotate@keyframes
.mockup { animation: float $duration $easing infinite; } // $easing: $ease-spring

@keyframes float {
  0%   { transform: translateY(0) rotate(0deg); }
  50%  { transform: translateY(-16px) rotate(-1.2deg); }
  100% { transform: translateY(0) rotate(0deg); }
}

05배지 슬라이드 인

NEW 배지가 옆에서 미끄러져 들어와 한동안 머물다 다시 빠져나가는 동작을 반복한다. 30%~65% 구간에 정지 상태를 넣어 뒀기 때문에 논스톱으로 왔다 갔다 하지 않고, 들어와서 잠깐 읽힐 시간을 준다.

transform: translateXopacitykeyframe hold
.badge { animation: slide-in $duration $easing infinite; } // $easing: $ease-pop

@keyframes slide-in {
  0%   { opacity: 0; transform: translateX(40px); }
  30%  { opacity: 1; transform: translateX(0); }
  65%  { opacity: 1; transform: translateX(0); }
  100% { opacity: 0; transform: translateX(40px); }
}

06마퀴 스트립

히어로 아래 연동사 알약을 두 벌 이어 붙인 트랙이 translateX(-50%) 로 끊김 없이 흐른다. 간격은 flexgap 이 아니라 알약마다 margin-right 로 준다 — gap 을 쓰면 세트 사이 이음새만 간격이 반 토막 나서 한 바퀴마다 픽셀이 튄다.

translateX(-50%)margin-right gapdoubled track
.marquee__track {
  display: flex;
  width: max-content;
  animation: pill-scroll $duration $easing infinite; // $easing: linear
}
.marquee__pill { margin-right: 16px; flex-shrink: 0; } // gap 대신 margin-right

@keyframes pill-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

07스크롤 시 텍스트가 뒤로 페이드

실제 서비스라면 scroll-timeline 이 헤드라인의 크기·투명도·위치를 스크롤 진행률에 직접 묶는다. 다만 브라우저 지원 차이가 아직 커서, 이 데모는 같은 모양을 @keyframes 반복 루프로 대신 보여준다.

scale · opacitytranslateYtransform-origin: center top
.hero { animation: recede $duration $easing infinite; transform-origin: center top; } // $easing: $ease-out

@keyframes recede {
  0%   { opacity: 1;   transform: scale(1)   translateY(0); }
  55%  { opacity: .15; transform: scale(.85) translateY(-40px); }
  100% { opacity: 1;   transform: scale(1)   translateY(0); }
}

08분할 텍스트 마스크 리빌

헤드라인이 왼쪽에서 오른쪽으로 쓸려 나오듯 드러난다. clip-path: inset() 의 오른쪽 여백을 100%에서 0%로 좁히는 방식이고, 같은 박자로 밑줄이 scaleX 로 자라 같이 등장한다.

clip-path: inset()scaleX underlinesweep reveal
.headline__mask { clip-path: inset(0 100% 0 0); animation: reveal $duration $easing infinite; } // $easing: $ease-spring

@keyframes reveal {
  0%   { clip-path: inset(0 100% 0 0); }
  40%  { clip-path: inset(0 0% 0 0); }
  85%  { clip-path: inset(0 0% 0 0); }
  100% { clip-path: inset(0 100% 0 0); }
}

09@starting-style 로드 진입

토스트 알림이 display: none 에서 화면에 나타나는 그 전환 자체를 @starting-styletransition-behavior: allow-discrete 만으로 만든다. @keyframes 도, 진입 애니메이션을 위한 JS 도 필요 없다 — 데모의 8줄짜리 JS 는 이 등장을 반복 재생하려고 넣은 것일 뿐, 실제 서비스에서 토스트가 한 번 새로 뜨는 순간엔 이 JS 없이도 그대로 작동한다.

@starting-styletransition-behavior: allow-discretedisplay transition
@supports (transition-behavior: allow-discrete) {
  .toast {
    transition: display $duration allow-discrete, opacity $duration $easing, transform $duration $easing;
  }
  .toast.is-hidden {
    display: none;
    opacity: 0;
    transform: translateY($sp-3) scale(.92);
  }
  @starting-style {
    .toast { opacity: 0; transform: translateY($sp-3) scale(.92); }
  }
}

어디서 깨지는가 — @starting-style 은 이미 있던 요소에도 걸린다

09번을 처음 만들 때는 @starting-style 이 나중에 DOM 에 추가되는 요소에만 걸릴 거라고 생각했다. 실측해 보니 아니었다 — 페이지를 열자마자, 반복 재생용 JS 가 단 한 번도 돌기 전에 토스트가 이미 진입 전환을 한 번 재생했다. @starting-style 은 "새로 렌더된 것처럼 보이는" 모든 순간에 적용되고, 여기엔 페이지가 막 그려진 첫 프레임도 포함된다. 그래서 반복 재생 JS 는 등장을 "처음 만드는" 코드가 아니라 이미 CSS 가 한 번 해낸 걸 "다시 보여주는" 코드에 가깝다. 이 9개 파일이 들어 있는 zip 의 압축 비밀번호는 5p7qvp7z 이고, 받은 파일에 그대로 입력하면 풀린다. 03번의 box-shadow, 08번의 clip-path 도 비슷한 계열의 함정을 갖고 있다 — 둘 다 GPU 가 합성만 하는 transform·opacity 와 달리 매 프레임 다시 칠해야 해서, 레이어를 욕심내 겹칠수록 저사양 기기에서 먼저 티가 난다.

접근성

reduced-motion 을 켠 환경에서는 아홉 개 모두 animation 이 통째로 꺼지도록 맞췄다. 01번은 줄이 곧바로 다 보이는 상태로, 02·04·07번은 움직임이 멈춘 정지 화면으로, 05·08번은 배지·헤드라인이 이미 다 드러난 최종 상태로 남는다. 09번은 전환(transition)만 꺼서 토스트가 항상 보이는 상태를 유지한다 — 움직임이 불편한 사람도 정보 자체는 놓치지 않는다.

FAQ

CSS 만으로 히어로 섹션 애니메이션을 만들 수 있나요?

네, 9개 전부 CSS 만으로 움직인다. 유일하게 09번이 데모를 반복 재생하려고 8줄짜리 JS 를 곁들였을 뿐, 실제 서비스에서 토스트 하나가 새로 뜨는 순간에는 이 JS 자체가 필요 없다.

@starting-style 은 어느 브라우저에서 되나요?

Chrome·Edge 117 이상에서 된다. MDN 의 @starting-style 문서 에 지원 범위가 있고, 미지원 브라우저에서는 09번 데모가 @supports 블록 밖으로 빠져 전환 없이 항상 보이는 상태로만 남는다 — 요소 배치 자체는 그대로 유지된다.

스크롤 애니메이션인데 왜 실제로 스크롤을 안 해도 움직이나요?

07번은 실제 서비스라면 scroll-timeline 으로 스크롤 진행률에 직접 묶이지만, 이 데모는 캡처와 브라우저 호환을 위해 같은 모양을 시간 기반 @keyframes 루프로 대신 재생한다. 실제 붙일 때는 MDN 의 scroll-driven animations 문서 를 참고해 animation-timeline: view() 로 바꾸면 된다. 나머지 CSS 애니메이션 모음은 사이트 검색 에서, 이 블로그가 데모를 만드는 방식은 소개 페이지 에서 볼 수 있다.

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

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