GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

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

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

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

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

01헤드라인 순차 등장

히어로 섹션 디자인에서 어디에 쓰나

헤드라인 세 줄이 한꺼번에 뜨지 않고 한 줄씩 늦게 등장한다.

헤드라인 순차 등장
새 창에서 열기

01 · 헤드라인 순차 등장 — 측정 누적면적 13.299% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@keyframesopacity · translateYstagger delay

동작 원리

줄마다 따로 @keyframes 를 두고 시작 지점을 10%·20%·30% 로 어긋나게 배치해, opacity 와 translateY 두 값만으로 순서를 만든다.

.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 으로 가둔 뒤, translate 와 rotate 만으로 천천히 돌린다.

배경 그라디언트 드리프트
새 창에서 열기

02 · 배경 그라디언트 드리프트 — 측정 누적면적 23.185% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

::beforeconic-gradienttranslate · rotate

만드는 방식

background-position 을 움직이는 방식과 달리 카드 크기가 바뀌어도 그라디언트가 다시 계산되지 않는다.

.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 펄스 글로우

어디에 쓰나

버튼이 scale 과 box-shadow 를 같이 오가며 숨 쉬듯 커졌다 작아진다.

CTA 펄스 글로우
새 창에서 열기

03 · CTA 펄스 글로우 — 측정 누적면적 8.864% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadowscale:hover · :focus-visible

왜 이렇게 만들었나

데모용 반복 재생과 별개로 실제 :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 를 오가며 둥둥 떠 있는 것처럼 보인다.

목업 플로트
새 창에서 열기

04 · 목업 플로트 — 측정 누적면적 16.142% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: translateYrotate@keyframes

구현의 핵심

두 값 다 이동(transform)만 건드려서 레이아웃이 다시 계산될 일이 없다.

.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 배지가 옆에서 미끄러져 들어와 한동안 머물다 다시 빠져나가는 동작을 반복한다.

배지 슬라이드 인
새 창에서 열기

05 · 배지 슬라이드 인 — 측정 누적면적 1.14% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: translateXopacitykeyframe hold

무엇이 핵심인가

30%~65% 구간에 정지 상태를 넣어 뒀기 때문에 논스톱으로 왔다 갔다 하지 않고, 들어와서 잠깐 읽힐 시간을 준다.

.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%) 로 끊김 없이 흐른다.

마퀴 스트립
새 창에서 열기

06 · 마퀴 스트립 — 측정 누적면적 7.556% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX(-50%)margin-right gapdoubled track

짚어 둘 점

간격은 flex 의 gap 이 아니라 알약마다 margin-right 로 준다 — gap 을 쓰면 세트 사이 이음새만 간격이 반 토막 나서 한 바퀴마다 픽셀이 튄다.

.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 이 헤드라인의 크기·투명도·위치를 스크롤 진행률에 직접 묶는다.

스크롤 시 텍스트가 뒤로 페이드
새 창에서 열기

07 · 스크롤 시 텍스트가 뒤로 페이드 — 측정 누적면적 13.056% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale · opacitytranslateYtransform-origin: center top

어떻게 만들었나

다만 브라우저 지원 차이가 아직 커서, 이 데모는 같은 모양을 @keyframes 반복 루프로 대신 보여준다.

.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분할 텍스트 마스크 리빌

어떻게 보이나

헤드라인이 왼쪽에서 오른쪽으로 쓸려 나오듯 드러난다.

분할 텍스트 마스크 리빌
새 창에서 열기

08 · 분할 텍스트 마스크 리빌 — 측정 누적면적 2.719% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

clip-path: inset()scaleX underlinesweep reveal

코드 읽는 법

clip-path: inset() 의 오른쪽 여백을 100%에서 0%로 좁히는 방식이고, 같은 박자로 밑줄이 scaleX 로 자라 같이 등장한다.

.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-style 과 transition-behavior: allow-discrete 만으로 만든다.

@starting-style 로드 진입
새 창에서 열기

09 · @starting-style 로드 진입 — 측정 누적면적 2.515% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@starting-styletransition-behavior: allow-discretedisplay transition

코드의 요점

@keyframes 도, 진입 애니메이션을 위한 JS 도 필요 없다 — 데모의 8줄짜리 JS 는 이 등장을 반복 재생하려고 넣은 것일 뿐, 실제 서비스에서 토스트가 한 번 새로 뜨는 순간엔 이 JS 없이도 그대로 작동한다.

@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 은 이미 있던 요소에도 걸린다

무엇이 문제인가

히어로 섹션 ui 를 짜며 09번을 처음 만들 때는 @starting-style 이 나중에 DOM 에 추가되는 요소에만 걸릴 거라고 생각했다. 실측해 보니 아니었다 — 페이지를 열자마자, 반복 재생용 JS 가 단 한 번도 돌기 전에 토스트가 이미 진입 전환을 한 번 재생했다. @starting-style 은 "새로 렌더된 것처럼 보이는" 모든 순간에 적용되고, 여기엔 페이지가 막 그려진 첫 프레임도 포함된다.

어떻게 피하나

첫 화면 연출을 컴포넌트 안으로 옮길 때 생기는 재실행 문제는 라이브러리 없는 리액트 CSS 애니메이션에서 마운트 시점 기준으로 정리했습니다.

그래서 반복 재생 JS 는 등장을 "처음 만드는" 코드가 아니라 이미 CSS 가 한 번 해낸 걸 "다시 보여주는" 코드에 가깝다. 이 9개 파일이 들어 있는 zip 의 압축 비밀번호는 5p7qvp7z 이고, 받은 파일에 그대로 입력하면 풀린다. 03번의 box-shadow, 08번의 clip-path 도 비슷한 계열의 함정을 갖고 있다 — 둘 다 GPU 가 합성만 하는 transform·opacity 와 달리 매 프레임 다시 칠해야 해서, 레이어를 욕심내 겹칠수록 저사양 기기에서 먼저 티가 난다.

접근성

reduced-motion 에서 남는 것

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 애니메이션 모음은 사이트 검색 에서, 이 블로그가 데모를 만드는 방식은 소개 페이지 에서 볼 수 있다. 행사 페이지처럼 날짜와 신청 버튼이 함께 붙는 첫 화면이 필요하면 이벤트 페이지 템플릿에 완성형 섹션이 있습니다.

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

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