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

CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + React

CSS 버튼 애니메이션 아홉 개를 한 번에 모았습니다. 호버, 클릭, 로딩, 실패, 확인까지 버튼 하나가 겪는 상황을 아홉 장면으로 나눠 하나씩 담았고, 전부 transform과 opacity만 움직입니다.

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

아홉 개는 흔히 보는 순서가 아니라 버튼이 겪는 순서로 놓았습니다. 눈에 띄어야 하고(01~05), 눌렀을 때 답해야 하고(06~07), 실패했을 때 말해야 하고(08), 위험한 동작은 한 번 더 물어야 합니다(09). 항목마다 코드는 핵심 부분만 실었고, 전체 파일은 zip에 있습니다. 움직임이 아니라 버튼의 생김새부터 정하고 싶다면 css 버튼 디자인 9종을 먼저 펼쳐 보시고, 06 로딩 전환처럼 기다림을 보여주는 화면 전체가 필요하면 스켈레톤 UI CSS 9종이 그 뒷장입니다.

01리플

무엇이 보이나

누른 자리에서 원형 물결이 퍼지며 사라집니다.

리플
새 창에서 열기

01 · 리플 — 측정 누적면적 9.62% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

600ms느리게 900ms어두운 물결

어디에 쓰나

결제·확인처럼 "눌렀다"가 확실해야 하는 버튼에 씁니다.

.btn {
  position: relative; overflow: hidden; isolation: isolate;
  &::after {
    content: ""; position: absolute;
    left: var(--x, 50%); top: var(--y, 50%);
    width: var(--d, 0); height: var(--d, 0);
    border-radius: 50%; background: $ripple;
    transform: translate(-50%, -50%) scale(0); opacity: 0;
  }
  &.is-rippling::after { animation: ripple $duration $easing forwards; }
}
@keyframes ripple { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }

02떠오름

화면에서 일어나는 일

호버하면 6px 떠오르고 그림자가 커집니다.

떠오름
새 창에서 열기

02 · 떠오름 — 측정 누적면적 6.583% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

6px높게 10px그림자 없음

css 버튼 호버 애니메이션은 어디에 쓰나

카드형 버튼이나 상품 담기처럼 "잡을 수 있다"는 느낌이 필요한 곳에 씁니다.

$lift: 6px; $duration: 220ms; $shadow: rgba(23, 20, 26, .32);
.btn {
  will-change: transform;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1), box-shadow $duration;
  &:hover, &:focus-visible { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
  &:active { transform: translateY(-2px); box-shadow: 0 6px 12px $shadow; }
}

03밑줄 슬라이드

어떤 움직임인가

밑줄이 왼쪽부터 그어지고, 커서가 떠나면 오른쪽으로 빠집니다.

밑줄 슬라이드
새 창에서 열기

03 · 밑줄 슬라이드 — 측정 누적면적 0.339% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

2px굵게 3px가운데서

언제 쓰나

텍스트형 링크 버튼과 내비게이션에 씁니다.

.link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: $thickness; background: currentColor;
  transform: scaleX(0); transform-origin: right;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1);
}
.link:hover::after { transform: scaleX(1); transform-origin: left; }

04자석

무엇이 보이나

커서가 반경 안에 들어오면 버튼이 커서 쪽으로 살짝 끌려갑니다.

자석
새 창에서 열기

04 · 자석 — 측정 누적면적 9.387% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

0.35강하게 0.6반경 120px

어디에 쓰나

랜딩 페이지에서 단 하나뿐인 CTA에만 씁니다. 두 개 이상이면 장난감이 됩니다.

document.addEventListener('pointermove', function (e) {
  var r = btn.getBoundingClientRect();
  var dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
  if (Math.hypot(dx, dy) < radius) btn.style.transform = 'translate(' + dx * strength + 'px,' + dy * strength + 'px)';
  else btn.style.transform = '';
});

05테두리 회전

화면에서 일어나는 일

그라데이션 테두리가 버튼 둘레를 계속 돕니다.

테두리 회전
새 창에서 열기

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

3s빠르게 1.5s2색

쓰는 자리

AI 기능이나 프리미엄처럼 "이건 특별하다"를 말해야 하는 버튼 하나에 씁니다.

.btn {
  position: relative; overflow: hidden; isolation: isolate;
  padding: $border; border-radius: 16px;
  &::before {
    content: ""; position: absolute; inset: -60%;
    background: conic-gradient(from 0deg, $colors);
    animation: spin $duration linear infinite; z-index: -1;
  }
}
.btn__in { display: block; border-radius: 13px; background: #17141a; }
@keyframes spin { to { transform: rotate(360deg); } }

06로딩 전환

어떤 움직임인가

누르면 글자가 사라지고 스피너가 나타납니다.

로딩 전환
새 창에서 열기

06 · 로딩 전환 — 측정 누적면적 1.013% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

300ms폭 유지체크로 끝

언제 쓰나

저장·전송처럼 기다려야 하는 동작에 씁니다. 폭은 그대로 두어야 옆 요소가 안 밀립니다.

.btn { display: grid; place-items: center; min-width: 200px; }
.btn__label, .btn__spin { grid-area: 1 / 1; transition: opacity $duration, transform $duration; }
.btn__spin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, .35); border-top-color: #fff;
  opacity: 0; transform: scale(.6); animation: spin $spin linear infinite;
}
.btn.is-loading .btn__label { opacity: 0; transform: translateY(6px); }
.btn.is-loading .btn__spin  { opacity: 1; transform: scale(1); }

07화살표 밀기

무엇이 보이나

호버하면 화살표 선이 길어지며 화살촉이 오른쪽으로 밀립니다.

화살표 밀기
새 창에서 열기

07 · 화살표 밀기 — 측정 누적면적 0.186% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

8px멀리 14px선 없이

어디에 쓰나

다음·더 보기·계속 버튼에 씁니다. 아이콘 폰트 없이 선 하나와 꺾인 테두리 하나로 그립니다.

.arrow__line { transform-origin: left; transform: scaleX(.55); transition: transform $duration; }
.arrow__head {
  width: 12px; height: 12px;
  border-top: 2.5px solid currentColor; border-right: 2.5px solid currentColor;
  transform: rotate(45deg); transition: transform $duration;
}
.btn:hover .arrow__line { transform: scaleX(1); }
.btn:hover .arrow__head { transform: rotate(45deg) translate($push * .7, -$push * .7); }

08흔들림

화면에서 일어나는 일

틀렸을 때 좌우로 세 번 흔들리고 멈춥니다.

흔들림
새 창에서 열기

08 · 흔들림 — 측정 누적면적 2.381% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

6px세게 10px두 번만

쓰는 자리

로그인 실패나 잘못된 입력에 씁니다. 다섯 번 넘게 흔들면 화난 것처럼 보입니다.

.btn.is-shaking { animation: shake $duration cubic-bezier(.36, .07, .19, .97) both; }
@keyframes shake {
  10%, 90% { transform: translateX(-$amount * .5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX(-$amount * .7); }
}

09길게 눌러 확인

어떤 움직임인가

누르고 있는 동안 배경이 차오르고 다 차면 실행되며, 중간에 떼면 되돌아갑니다.

길게 눌러 확인
새 창에서 열기

09 · 길게 눌러 확인 — 측정 누적면적 8.162% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

1.2s빠르게 0.8s세로로

언제 쓰나

삭제·탈퇴처럼 실수하면 안 되는 동작에 확인 모달 대신 씁니다. JS는 pointerdown에 is-holding을 켜고 pointerup에 끄며, 채우기 애니메이션의 animationend가 실행 신호입니다.

.btn__fill {
  position: absolute; inset: 0; background: $fill;
  transform: scaleX(0); transform-origin: left; z-index: -1;
  transition: transform 200ms ease-out;
}
.btn.is-holding .btn__fill { animation: fill $hold linear forwards; }
@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

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

레이아웃을 건드리지 않는 이유

css 버튼 클릭 애니메이션까지 아홉 개 전부 transform과 opacity만 움직이는 데는 이유가 있습니다. width나 top을 애니메이션하면 브라우저가 매 프레임 레이아웃을 다시 계산하지만, transform과 opacity는 합성 단계에서만 처리됩니다. MDN의 애니메이션 성능 문서가 이 둘을 권하는 이유입니다. 06 로딩 전환에서 버튼 폭을 줄이고 싶어지는데, 그 순간 이 규칙이 깨지고 옆 요소까지 흔들립니다. 폭은 유지하고 안쪽만 바꾸세요. 06 로딩 링처럼 "응답을 기다리는 중"을 버튼 안에서 말해야 하는 자리가 늘어나면, 답이 도착하기까지의 시간을 통째로 다루는 AI 채팅 로딩 애니메이션 쪽이 레이아웃을 건드리지 않고 더 길게 버팁니다.

물결의 크기

함정은 아니지만 덧붙일 것이 둘 있습니다. 하나는 크기입니다. 01 리플을 처음 만들 때 지름을 320px로 두고 작은 버튼에 얹었더니, 원이 두 프레임 만에 버튼을 다 덮어서 물결이 아니라 "잠깐 밝아짐"으로 보였습니다. 물결 지름은 버튼 긴 변의 두 배로 계산하고, 퍼지는 시간은 600ms 밑으로 줄이지 마세요. 이 값들이 이미 들어간 아홉 개 파일의 압축 비밀번호는 xu8ewsq8 이고, 아래 버튼으로 받을 수 있습니다.

모바일 호버

다른 하나는 모바일 호버입니다. 02·03·07은 호버가 없는 터치 화면에서는 아무 반응이 없습니다. 그래서 zip의 파일들은 :focus-visible에도 같은 동작을 걸어 두었고, 터치에서는 눌렀을 때 :active로 대신합니다.

접근성

동작 줄이기를 켰을 때

시스템에서 "동작 줄이기"를 켠 사용자를 위해 아홉 개 전부 prefers-reduced-motion: reduce를 처리합니다. 움직임은 끄되 피드백은 남기는 게 원칙입니다. 02는 뜨지 않고 그림자만 바뀌고, 06은 링이 돌지 않으며, 08은 흔들림 대신 색이 바뀝니다. 09는 예외입니다. 길게 누르기는 장식이 아니라 안전장치라서 채우기를 유지합니다. 이 미디어 쿼리의 지원 범위는 MDN prefers-reduced-motion에서 확인할 수 있습니다.

@media (prefers-reduced-motion: reduce) {
  .btn.is-demo, .btn.is-shaking { animation: none; }
  .btn:hover { transform: none; }
}

css 버튼 만들기에서 한 걸음 더 나가는 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

css 버튼 디자인 아홉 개를 한 프로젝트에 다 넣어도 되나요?

넣을 수는 있지만 한 화면에는 두 종류 이하가 좋습니다. 04 자석과 05 테두리 회전은 "특별한 버튼 하나"용이라 페이지에 하나씩만 두세요. 나머지는 상황별이라 겹칠 일이 적습니다. 한 화면에 두는 특별한 버튼을 하나 더 고른다면 다크모드 토글 애니메이션처럼 상태가 바뀌는 스위치가 04·05와 성격이 겹치지 않아 함께 두기 좋습니다.

React에서는 클래스 토글 대신 뭘 쓰나요?

zip의 react/ 폴더는 상태를 props로 받습니다. 06은 loading, 08은 실패 횟수 shakeKey, 09는 onConfirm 콜백입니다. SCSS 모듈은 var()만 읽으므로 변수 세 개를 props로 넘기면 됩니다.

애니메이션이 끊기는 것 같아요.

먼저 transform과 opacity 외의 속성을 애니메이션하고 있지 않은지 확인하세요. 그다음 will-change: transform을 움직이는 요소에만 붙이세요. 모든 요소에 붙이면 오히려 느려집니다.

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

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