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

CSS 카드 애니메이션 호버 효과 9종 — 한 줄 복붙으로

CSS 카드 애니메이션은 카드 컴포넌트가 마우스나 포커스에 반응해 뜨거나 빛나거나 기울어지는 짧은 시각 변화를 말합니다. 카드 호버 효과 아홉 가지를 한 번에 모았고, 커서 좌표가 필요한 두 개는 JS를 10줄 안팎만 씁니다.

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

아홉 개는 인기 순위가 아니라 카드 리스트 ui 에서 카드를 고르는 사람의 눈에 걸리는 순서로 놓았습니다. 먼저 존재감을 드러내고(01~02), 기울고 내용을 더 보여주고(03~05), 눌러보라고 몸짓하고(06~08), 마지막으로 실제로 눌렀을 때 답합니다(09). 아래 코드는 핵심만 옮겼고 완결본은 zip에 있습니다. 카드 대신 대화 화면을 꾸민다면 채팅 말풍선 CSS 애니메이션이, 07 그라데이션 테두리를 글자에까지 흘리고 싶다면 CSS 글자 그라데이션 반짝이는 텍스트가 이어지는 자리입니다.

01들림+그림자

마우스 호버 효과로 무엇이 달라지나

카드에 마우스를 올리면 8px 위로 떠오르며 그림자가 깊어져, 눌리기 전인데도 그리드 안에서 먼저 도드라집니다.

어디에 쓰나

상품 카드나 글 목록처럼 여러 장이 나란히 놓이는 자리에 씁니다.

들림+그림자
새 창에서 열기

01 · 들림+그림자 — 측정 누적면적 11.669% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: translateYbox-shadow 확대transition
.card {
  transform: translateY(0);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.card:hover,
.card:focus-visible,
.card:active {
  transform: translateY(-8px);
  box-shadow: $shadow-raised;
}

02테두리 빛 회전

어떻게 움직이나

카드 테두리 안쪽에 conic-gradient 빛 한 줄기를 두고 카드에 overflow: hidden을 걸어 가둔 채 2초마다 한 바퀴 돌립니다.

어디에 쓰나

각도는 @property로 등록한 --angle을 keyframes로 0deg에서 360deg까지 옮겨 매끄럽게 이어지게 했습니다.

테두리 빛 회전
새 창에서 열기

02 · 테두리 빛 회전 — 측정 누적면적 1.778% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

conic-gradient@property angleopacity
@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.card__ring {
  background: conic-gradient(from var(--angle), transparent 0deg, $color 40deg, transparent 100deg);
  opacity: 0;
}
.card:hover .card__ring,
.card.is-demo .card__ring {
  opacity: 1;
  animation: spin $duration linear infinite;
}
@keyframes spin { to { --angle: 360deg; } }

033D 틸트 시차

어떻게 움직이나

커서가 카드 위 어디 있는지에 따라 rotateX·rotateY로 기울고, 배경 레이어는 반대 방향으로 살짝 밀려 층이 갈라진 것처럼 보입니다.

어디에 쓰나

좌표 계산은 JS 여덟 줄이 맡고 CSS는 그 값을 변수로만 받습니다.

3D 틸트 시차
새 창에서 열기

03 · 3D 틸트 시차 — 측정 누적면적 11.272% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

perspectiverotateX/rotateYJS 좌표
card.addEventListener('pointermove', function (e) {
  var r = card.getBoundingClientRect();
  var px = (e.clientX - r.left) / r.width - .5;
  var py = (e.clientY - r.top) / r.height - .5;
  card.style.setProperty('--rx', (-py * 14) + 'deg');
  card.style.setProperty('--ry', (px * 16) + 'deg');
  card.style.setProperty('--pz', (px * -12) + 'px');
});

04내용 슬라이드업

어떻게 움직이나

이미지 아래 숨어 있던 안내 패널이 호버하면 이미지 위로 슬라이드업해, 카드 크기는 그대로 둔 채 정보만 하나 더 보여줍니다.

내용 슬라이드업
새 창에서 열기

04 · 내용 슬라이드업 — 측정 누적면적 11.14% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: translateYoverflow: hiddenopacity
.card__panel {
  transform: translateY(70%);
  transition: transform $duration $easing;
}
.card:hover .card__panel,
.card.is-demo .card__panel {
  transform: translateY(0);
}

05배경 이미지 확대

어떻게 움직이나

카드를 덮은 배경 전체가 1.12배 확대되고, overflow: hidden이 카드의 둥근 모서리 밖을 잘라냅니다.

어디에 쓰나

매끈한 그러데이션만으로는 확대가 잘 안 보여서 해와 언덕 모양의 도형을 더했습니다.

배경 이미지 확대
새 창에서 열기

05 · 배경 이미지 확대 — 측정 누적면적 7.676% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: scaleoverflow: hiddenborder-radius 유지
.card { overflow: hidden; }
.card__bg {
  transform: scale(1);
  transition: transform $duration $easing;
}
.card:hover .card__bg,
.card.is-demo .card__bg {
  transform: scale(1.12);
}

06아이콘 튀어오름

어떻게 움직이나

카드 모서리의 원형 화살표 배지가 호버하면 공처럼 두 번 튀어 올라, 눌러도 된다는 걸 몸짓으로 먼저 보여줍니다.

아이콘 튀어오름
새 창에서 열기

06 · 아이콘 튀어오름 — 측정 누적면적 2.056% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@keyframes bouncecubic-beziertransform: translateY
.card__icon { transform: translateY(0); }
.card:hover .card__icon,
.card:focus-visible .card__icon,
.card:active .card__icon {
  animation: bounce $duration $easing 2;
}
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}

07그라데이션 테두리

어떻게 움직이나

평소엔 얇은 회색 테두리인 카드가 호버하면 두 색 그라데이션 테두리로 바뀝니다.

어디에 쓰나

mask-composite로 안쪽만 파낸 층의 opacity만 0에서 1로 바꿔, 레이아웃은 전혀 흔들리지 않습니다.

그라데이션 테두리
새 창에서 열기

07 · 그라데이션 테두리 — 측정 누적면적 2.281% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

mask-compositelinear-gradientopacity
.card__border {
  background: linear-gradient(135deg, $color, $subject-cream);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity $duration $easing;
}
.card:hover .card__border,
.card.is-demo .card__border {
  opacity: 1;
}

08커서 스포트라이트

어떻게 움직이나

커서 좌표를 CSS 변수 두 개로 넘기면 radial-gradient 빛이 그 지점에서 카드 표면을 비춥니다.

어디에 쓰나

--mx --my --sp 세 값을 @property로 등록해 스포트라이트가 순간 이동 없이 미끄러지듯 움직이게 했습니다.

커서 스포트라이트
새 창에서 열기

08 · 커서 스포트라이트 — 측정 누적면적 9.937% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermoveCSS 변수radial-gradient
card.addEventListener('pointermove', function (e) {
  var r = card.getBoundingClientRect();
  card.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
  card.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
  card.style.setProperty('--sp', 1);
});

09눌림 피드백

어떻게 움직이나

카드를 실제로 누르면 scale(1)이 scale(.97)로 살짝 줄고 그림자도 얕아져, 손끝에 닿았다는 확인을 줍니다.

어디에 쓰나

호버로는 반응하지 않고 누르는 동안에만 움직입니다.

눌림 피드백
새 창에서 열기

09 · 눌림 피드백 — 측정 누적면적 2.287% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

:activetransform: scalebox-shadow 축소
.card {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.card:active {
  transform: scale(.97);
  box-shadow: $shadow-press;
}

어디서 깨지는가 — 함정 하나

안 움직이는 것으로 잡혔다

05 배경 확대를 처음 만들 때는 파란색에서 크림색으로 흐르는 매끈한 linear-gradient 하나만 배경에 깔고 scale(1.12)만 걸었습니다. 렌더한 뒤 측정값을 열어 보니 누적면적 0.00%·강도 0.0, 스물세 프레임이 전부 똑같았습니다.

왜 걸러졌나

프레임 두 장을 직접 픽셀 단위로 비교하자 실제로는 최대 11만큼 색이 바뀌고 있었는데, 설정의 픽셀문턱이 12라서 전부 "변화 없음"으로 걸러진 것이었습니다. MDN의 linear-gradient() 문서를 다시 읽어 보면 알 수 있듯, 매끈한 그러데이션은 확대해도 이웃 픽셀끼리 색이 너무 가까워 문턱을 못 넘습니다.

어떻게 고쳤나

경계가 뚜렷한 원과 곡선 도형을 그러데이션 위에 얹었더니 같은 배율에서 강도 41.3·누적면적 0.68%로 정상 통과했습니다. 처음부터 경계를 두껍게 긋고 그림자를 번지지 않게 쓰는 네오브루탈리즘 UI 랜딩 부품은 이 픽셀문턱 문제를 반대쪽에서 피해 간 예입니다. 이 기록이 담긴 압축 파일의 비밀번호는 45c9tna8이고, 몰랐다면 05는 지금도 "안 움직이는 카드"로 남아 있었을 겁니다.

접근성

동작 줄이기에서

아홉 개 전부 prefers-reduced-motion: reduce에서 움직임만 끄고 마지막 피드백은 남깁니다. 01은 뜨지 않고 그림자만 짙게, 02는 빛이 돌지 않고 옅게 고정, 03은 기울지 않되 :focus-visible에서 기본 각도만 보여주고, 04는 패널이 처음부터 펼쳐진 채로, 05는 확대 없이 정지, 06은 배지가 튀지 않고, 07은 전환만 빠져 테두리가 즉시 켜지고 꺼지고, 08은 빛이 꺼진 채 멈추고, 09는 그림자만 얕아집니다.

키보드와 터치

:hover뿐 아니라 :focus-visible·:active에도 같은 최종 상태를 걸어 키보드·터치 사용자가 마우스 사용자와 같은 결과를 봅니다. @property를 쓴 02·08의 각도·좌표 보간은 caniuse의 @property 지원표에서 Chrome·Safari·Firefox 모두 확인할 수 있습니다. 카드 말고 커서·타이핑처럼 다른 피드백 효과가 궁금하면 분류 페이지에서 더 찾을 수 있고, 이 사이트가 무엇을 주는지는 소개 페이지에 적어 두었습니다.

FAQ

08 스포트라이트는 터치 화면에서도 커서를 따라갑니까?

아니요, pointermove는 터치에서 좌표를 계속 주지 않아 08은 --sp 기본값 0으로 멈춰 있습니다. 대신 :focus-visible·:active에서 카드 중앙에 고정 스포트라이트를 켜서 손가락으로 눌러도 반응은 남게 했습니다.

css 호버 효과 아홉 개를 한 화면에 다 같이 써도 됩니까?

됩니다만 02·08처럼 계속 도는 애니메이션은 한 화면에 한둘만 두는 게 낫습니다. 나머지 일곱 개는 호버·포커스·프레스 같은 사용자 동작에 걸려 있어 여러 장을 함께 둬도 화면이 산만해지지 않습니다. 카드 안에 켜고 끄는 스위치를 얹을 계획이면 09 눌림 피드백과 CSS 토글 스위치를 같은 duration 값으로 맞춰 두세요.

React 판에서 03·08의 커서 좌표는 어떻게 옮겼습니까?

두 컴포넌트 모두 useRef로 카드 엘리먼트를 잡고 useCallback으로 감싼 pointermove 핸들러 안에서 style.setProperty로 CSS 변수를 직접 씁니다. state를 거치지 않아 좌표가 바뀔 때마다 컴포넌트가 다시 렌더되지 않습니다.

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

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