GODRICH

CSS 가격표 디자인 애니메이션 9종

가격표 디자인에 마우스를 올렸을 때 카드가 살짝 떠오르거나 가격 숫자가 휙 바뀌는 걸 본 적 있으신가요? 어떤 요금제를 고를지 망설이는 그 짧은 순간, 이런 작은 움직임 하나가 시선을 붙잡아 줍니다.

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

아홉 개는 인기 순이 아니라 방문자가 가격표를 훑어보는 순서를 따라갑니다. 먼저 화면에 들어오면서 눈에 띄는 신호(01~03)를 지나, 실제로 손가락을 움직여 결제 주기를 바꾸는 조작 구간(04~06)으로, 그다음은 포함된 기능을 훑고 플랜끼리 비교하는 확인 구간(07~08)을 거쳐, 마지막은 결제 버튼을 누르도록 이끄는 CTA(09)로 끝납니다.

01스크롤 리빌 플랜

가격표가 있는 페이지를 스크롤해서 내려가면 플랜 카드들이 한꺼번에 뚝 나타나는 대신 순서대로 하나씩 떠오르며 등장합니다. IntersectionObserver가 그리드 전체가 화면에 들어오는 순간을 감지해 in 클래스를 붙이고, 카드마다 transition-delay만 다르게 둬 같은 움직임이 시차를 두고 반복됩니다.

IntersectionObservertransition-delayJS 7줄
var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (e.isIntersecting) { grid.classList.add('in'); io.unobserve(grid); }
  });
}, { threshold: 0.3 });
io.observe(grid);
.sr-card { opacity: 0; transform: translateY($sp-4); transition: opacity 400ms, transform 400ms; }
.sr-grid.in .sr-card { opacity: 1; transform: none; }
.sr-grid.in .sr-card:nth-child(2) { transition-delay: 140ms; }
.sr-grid.in .sr-card:nth-child(3) { transition-delay: 280ms; }

02배지 광택 스윕

인기 배지 위로 가늘고 밝은 빛줄기가 몇 초마다 한 번씩 스쳐 지나가, 마우스를 올리지 않아도 자연스럽게 눈길이 갑니다. 배지 안쪽에 overflow: hidden을 걸어두고 그 안의 그라디언트 띠만 transform: translateX로 이동시키는 방식이라 배지 크기 자체는 전혀 흔들리지 않습니다.

::aftertranslateX2.4s
.sh-badge { position: relative; overflow: hidden; }
.sh-badge::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .65), transparent);
  animation: sh-sweep 2.4s ease-in-out infinite;
}
@keyframes sh-sweep {
  0% { transform: translateX(-260%) skewX(-12deg); }
  60%, 100% { transform: translateX(360%) skewX(-12deg); }
}

03인기 플랜 들림 강조

이미 테두리와 배지로 강조돼 있는 인기 플랜 카드에 마우스를 올리면 카드가 한 번 더 떠오르며 그림자가 깊어집니다. transform: translateYbox-shadow만 함께 트랜지션해 옆에 놓인 다른 카드들은 전혀 흔들리지 않습니다.

translateYbox-shadow240ms
.pl-card--popular {
  box-shadow: $shadow-press;
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.pl-card--popular:hover {
  transform: translateY(-#{$sp-4});
  box-shadow: $shadow-raised;
}

04월간/연간 토글

Monthly와 Yearly 버튼 사이에서 알약 모양 배경이 미끄러지듯 이동해 지금 어떤 결제 주기를 고르고 있는지 보여줍니다. 숨긴 라디오 버튼의 :checked 상태를 일반 형제 선택자로 읽어 인디케이터를 옮길 뿐이라 자바스크립트가 한 줄도 없습니다.

:checkedtranslateX50%
.mt-indicator {
  position: absolute;
  width: calc(50% - #{$sp-1});
  height: calc(100% - #{$sp-1} * 2);
  transition: transform $duration $easing;
}
#mt-yearly:checked ~ .mt-indicator { transform: translateX(100%); }

05가격 숫자 플립

가격 숫자에 마우스를 올리면 카드가 휙 뒤집히듯 회전하며 뒷면에 적힌 다른 금액이 나타납니다. perspective를 준 부모 안에서 rotateXbackface-visibility: hidden을 가진 두 개의 면을 겹쳐 두고, 안쪽 레이어 하나만 회전시키는 방식입니다.

rotateXbackface-visibilityperspective
.pf-flip__inner {
  transform-style: preserve-3d;
  transition: transform $duration $easing;
}
.pf-flip:hover .pf-flip__inner { transform: rotateX(180deg); }
.pf-flip__face { position: absolute; inset: 0; backface-visibility: hidden; }
.pf-flip__face--back { transform: rotateX(180deg); }

06:has(:checked)로 JS 0 가격 전환

위 토글과 방식은 비슷해 보이지만, 이번엔 라디오 버튼의 선택 상태를 카드 전체에서 :has()로 읽어 월간·연간 가격 두 줄을 자연스럽게 교차 페이드시킵니다. 값을 갈아 끼우는 로직 전체가 셀렉터 하나로 끝나 자바스크립트가 필요 없습니다.

:has():checkedopacity
.hc-price { position: absolute; inset: 0; opacity: 0; transform: translateY($sp-2); }
.hc-price--monthly { opacity: 1; transform: none; }
.hc-card:has(.hc-radio--y:checked) .hc-price--monthly {
  opacity: 0;
  transform: translateY(-#{$sp-2});
}
.hc-card:has(.hc-radio--y:checked) .hc-price--yearly { opacity: 1; transform: none; }

07기능 체크 순차 등장

카드에 마우스를 올리면 포함된 기능 목록의 체크 표시가 위에서부터 한 줄씩 시차를 두고 나타납니다. 체크 아이콘은 선 두 개를 따로 그려 붙이지 않고, 요소 하나를 border-rightborder-bottom으로 L자로 만든 뒤 45도 회전시켜 두 선이 어긋날 일이 없게 했습니다.

transition-delayopacitytranslateX
.fc-check {
  width: 6px; height: 10px;
  border-right: 2px solid $color;
  border-bottom: 2px solid $color;
  transform: rotate(45deg);
}
.fc-row { opacity: 0; transform: translateX(-#{$sp-3}); transition: opacity 320ms, transform 320ms; }
.fc-card:hover .fc-row { opacity: 1; transform: none; }
.fc-card:hover .fc-row:nth-child(2) { transition-delay: 120ms; }
.fc-card:hover .fc-row:nth-child(3) { transition-delay: 240ms; }

08비교 열 강조

여러 플랜을 나란히 놓은 비교표에서 마우스를 올린 열만 테두리가 밝아지고 살짝 커져, 지금 보고 있는 플랜이 또렷해집니다. scaleborder-color만 트랜지션해 옆 열의 레이아웃은 건드리지 않습니다.

scaleborder-color180ms
.ch-col {
  border: 2px solid transparent;
  transition: transform 180ms, border-color 180ms, background 180ms;
}
.ch-col:hover {
  transform: scale(1.05);
  border-color: $color;
  background: rgba(23, 20, 26, .08);
}

09CTA 글로우

가격표 맨 아래 결제 버튼 뒤에서 부드러운 빛이 부풀었다 가라앉기를 반복하며 클릭할 곳을 조용히 알려줍니다. 02번 배지 스윕처럼 호버 없이 항상 움직이는 항목이라, 눈에 거슬리지 않도록 다른 항목보다 훨씬 은은하게 움직이도록 강도를 낮춰 맞췄습니다.

radial-gradientscale1.8s
.cg-btn::before {
  content: "";
  position: absolute;
  inset: -#{$sp-3};
  background: radial-gradient(circle, rgba(47, 109, 246, .9), transparent 65%);
  filter: blur(3px);
  animation: cg-pulse 1.8s $easing infinite;
}
@keyframes cg-pulse {
  0%, 100% { transform: scale(1); opacity: .2; }
  50% { transform: scale(1.45); opacity: .95; }
}

CSS 가격표 애니메이션은 어디서 깨지는가

가장 먼저 걸려 넘어진 건 09번이었습니다. 버튼 뒤로 글로우를 보내려고 별생각 없이 z-index: -1을 줬더니, .cg-btn에 자체 z-index가 없어 새 스태킹 컨텍스트가 생기지 않았고, 글로우가 가장 가까운 조상인 무대 배경보다도 아래로 밀려나 완전히 사라져 버렸습니다. z-index를 빼고 나서야 pseudo-element가 버튼 자기 배경 위, 즉 원래 있어야 할 자리로 돌아왔습니다. 06번에 쓴 :has() 셀렉터도 주의할 점이 있는데, Chrome·Edge·최신 Safari·Firefox에서는 잘 동작하지만 오래된 브라우저는 아예 그 규칙을 무시해 마지막에 적용된 값 그대로 멈춰 있습니다 — 데이터가 사라지진 않지만 전환 효과만 없어지는 정도라 실무에서는 크게 위험하지 않습니다. zip을 열 때 필요한 비밀번호는 prqmpcmj 자리에 본문 속 평범한 글자로 남겨 뒀으니, 이 문단을 천천히 읽으면 찾을 수 있습니다.

접근성 — 아홉 개가 reduced-motion에서 남기는 것

시스템에서 동작 줄이기를 켜면 아홉 개 전부 prefers-reduced-motion: reduce에서 애니메이션과 트랜지션을 끕니다. 02번 배지 스윕과 09번 CTA 글로우처럼 호버 없이 항상 움직이던 항목은 그냥 멈춘 상태로 보여도 정보 손실이 없고, 01번과 07번처럼 등장 자체가 정보인 항목은 움직임 대신 처음부터 opacity: 1로 고정해 카드와 체크 목록이 곧바로 다 보이게 했습니다. 04·05·06번은 값이 바뀌는 결과(요금·가격)가 중요하므로 전환 속도만 없앴을 뿐 최종 상태는 그대로 보여줍니다.

라디오와 :has()로 상태를 다루는 다른 예시가 궁금하다면 CSS 토글 스위치 글도 참고할 만하고, 전체 목록은 CSS 애니메이션 카테고리에서 모아 볼 수 있습니다. :has()의 정확한 지원 범위는 MDN 문서에서 확인할 수 있습니다.

FAQ — 자주 묻는 질문

가격표에 애니메이션을 넣으면 정말 전환율이 오르나요?

애니메이션 자체가 구매를 결정해 주지는 않습니다. 다만 인기 플랜이나 결제 버튼처럼 원래도 강조하고 싶던 요소에 자연스러운 움직임을 더하면 시선이 그쪽으로 먼저 가게 만드는 데는 도움이 됩니다.

04번 토글과 06번 :has() 방식 중 어느 쪽을 써야 하나요?

가격 자체는 그대로 두고 버튼 모양만 바꾸면 되는 단순한 화면이라면 04번 토글로 충분합니다. 월간·연간 가격 숫자까지 같은 자리에서 실제로 바뀌어야 한다면 06번처럼 :has()로 값 전환까지 CSS 안에서 끝내는 쪽이 자바스크립트 코드를 하나도 늘리지 않아도 됩니다.

zip 안의 vanilla와 react 코드가 다른가요?

화면에 보이는 움직임은 같습니다. vanilla 쪽 SCSS 변수 세 개($duration $easing $color 또는 항목에 맞는 동등물)를 React 판에서는 컴포넌트 props로 그대로 받아, 내부에서 CSS 커스텀 속성으로 바꿔 넘기는 구조만 다릅니다.

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

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