GODRICH

css 버튼 디자인 9종 — 캔디 그라데이션·눌림·로딩 버튼 코드

css 버튼 디자인은 버튼의 색과 모양, 마우스를 올렀을 때와 눌렀을 때의 반응까지 코드 몇 줄로 정하는 일이다. 어느 랜딩 페이지에서 본 캔디 같은 그라데이션 버튼, 손끝에 꾹 눌리는 입체 버튼을 내 사이트에 그대로 옮기고 싶을 때 이 아홉 가지는 SCSS 한 파일 복붙으로 끝난다.

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

아홉 개는 인기순이 아니라 버튼이 실제 화면에서 겪는 순서로 놓았다. 먼저 시선을 붙잡는 캔디 그라데이션(01)·빛줄기(02)·말랑 블롭(03)을 지나, 마우스를 올렀을 때 반응하는 테두리 그리기(04)·화살표 슬라이드(05)·듀오톤 와이프(06)가 따르고, 실제로 누르는 순간의 링 리플(07)·3D 눌림(08)을 거쳐 누른 뒤 기다리는 필 로더(09)로 끝난다.

01캔디 그라데이션 흐름 버튼

버튼보다 세 배 넓은 캔디색 천을 밑에 깔고 그것을 왼쪽으로 240px씩 밀어내는 방식이다. 움직이는 건 통째로 translateX 하나뿐이라 레이아웃 계산이 없고, 한 주기가 정확히 240px라 줄무늬가 이어지는 지점이 보이지 않는다. 랜딩 첫 화면의 가입 버튼에 어울린다.

repeating-linear-gradienttranslateX(-240px) 한 주기overflow: hidden
.btn__bg {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 720px;
  background: repeating-linear-gradient(to right,
    $color 0 58px, $candy-b 64px 118px, $candy-c 124px 178px, $color 184px 240px);
}
.stage.is-demo .btn__bg { animation: gradient-flow $duration $easing infinite; }
@keyframes gradient-flow {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-240px); }
}

02빛줄기 스윕 버튼

어두운 버튼 위로 기울어진 흰 빛 띠가 한 번 지나가며 유리에 빛이 스친 것 같은 하이라이트를 남긴다. 띠는 ::after 가상 요소이고 skewX(-20deg) 로 비스듬히 눕혀 translateX 로만 이동하므로 버튼 폭이 바뀌어도 그대로 돌아간다. 결제·구매처럼 한 번 더 눌러주고 싶은 버튼에 쓴다.

translateX(±150%)skewX(-20deg)overflow: hidden
.btn::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: -60%;
  width: 60%;
  background: linear-gradient(115deg,
    transparent 0%, rgba(255, 255, 255, .85) 50%, transparent 100%);
  transform: skewX(-20deg) translateX(-10%);
}
@keyframes sweep {
  0%, 10% { transform: skewX(-20deg) translateX(-10%); }
  55%     { transform: skewX(-20deg) translateX(310%); }
  100%    { transform: skewX(-20deg) translateX(310%); }
}

03말랑 블롭 버튼

라벨 뒤에 숨어 있던 민트색 동그라미 두 개가 좌우로 부풀며 버튼 윤곽이 젤리처럼 늘어났다 줄어들기를 반복한다. 동그라미는 버튼과 같은 색이라 경계가 합쳐지고, scale 로 부풀며 좌우로 살짝 밀리기에 겉으로 보이는 실루엣만 바뀐다. 키즈샵의 장바구니 담기 같은 귀여운 자리에 맞다.

::before·::after scaleborder-radius: 50%$ease-pop
.btn::before, .btn::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 56px; height: 56px;
  border-radius: 50%;
  background: $color;
  transform: translateY(-50%) scale(.3);
}
.stage.is-demo .btn::before { animation: blob-l $duration $easing infinite; }
@keyframes blob-l {
  0%, 12% { transform: translateY(-50%) scale(.3); }
  40%     { transform: translateY(-50%) scale(1.3) translateX(-8px); }
  100%    { transform: translateY(-50%) scale(.3); }
}

04테두리 그려지는 버튼

버튼 둘레를 손으로 그린 것처럼 한 획씩 그려 넣는다. SVG 네모 테두리에 pathLength="100" 속성을 주면 실제 길이가 몇 픽셀이든 백 칸짜리 자로 셀 수 있어서, stroke-dashoffset 을 100에서 0으로 줄이는 것만으로 그리기가 완성된다. 다 그려진 뒤 옅은 물감이 차오르는 것까지가 한 주기다.

stroke-dashoffsetpathLength=100vector-effect: non-scaling-stroke
.btn__rect {
  fill: none;
  stroke: $color;
  stroke-width: 3;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
}
.stage.is-demo .btn__rect { animation: draw $duration $easing infinite; }
@keyframes draw {
  0%, 12% { stroke-dashoffset: 100; }
  55%     { stroke-dashoffset: 0; }
  100%    { stroke-dashoffset: 100; }
}

05화살표 슬라이드 버튼

라벨만 있는 칸과 라벨에 화살표가 붙은 칸, 두 칸을 나란히 붙여 놓고 통째로 한 칸씩 민다. 마우스를 올리면 글자가 왼쪽으로 밀리면서 화살표가 오른쪽에서 제자리로 미끄러져 들어오고, 트랙의 폭을 200%로 고정해 뒀으니 어떤 글자 길이에도 어긋나지 않는다. 블로그의 더보기처럼 다음 내용으로 데려가는 버튼에 좋다.

격층 translateXoverflow: hidden$ease-spring
.btn__track { display: flex; width: 200%; height: 100%; }
.btn__cell  { flex: 0 0 50%; display: flex; align-items: center; }
.stage.is-demo .btn__track { animation: slide-in $duration $easing infinite; }
@keyframes slide-in {
  0%, 18%  { transform: translateX(0); }
  55%, 72% { transform: translateX(-50%); }
  88%      { transform: translateX(-4%); }
  100%     { transform: translateX(0); }
}

06듀오톤 와이프 버튼

버튼을 크림과 피치 두 색으로 쪼개고, 아래 색이 왼쪽에서 scaleX 로 펼쳐지며 물감이 번지듯 통째로 바뀐다. 글자도 바탕색에 맞춰 두 벌을 겹쳐 놓고 opacity 로 맞바꾸기 때문에 색이 넘어가는 순간에도 읽힌다. 요금제 카드의 시작하기처럼 골라 누르는 버튼에 어울린다.

scaleX 레이어 와이프transform-origin: leftcolor 전환
.btn__wash {
  position: absolute;
  inset: 0;
  background: $color;
  transform: scaleX(0);
  transform-origin: left;
}
@keyframes wash {
  0%, 12%  { transform: scaleX(0); }
  48%, 70% { transform: scaleX(1); }
  100%     { transform: scaleX(0); }
}
.stage.is-demo .btn__t-on-dark { animation: swap-in $duration $easing infinite; }

07링 리플 버튼

누르는 순간 물결이 아니라 종소리처럼 테두리 링이 바깥으로 퍼져 나간다. ::after 에 테두리 3px짜리 원을 겹쳐 두고 scale 로 키우며 opacity 를 지우는 것뿐이라 색만 바꾸면 어떤 아이콘 버튼에도 얹을 수 있다. 같은 순간 하트가 튀어올랐다 살짝 눌리므로 좋아요·북마크 같은 토글에 맞다.

::after 링 확산scale + opacity:active 트리거
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 3px solid $color;
  opacity: 0;
}
@keyframes ring {
  0%  { transform: scale(1);   opacity: .9; }
  34% { transform: scale(1.8); opacity: 0; }
  50% { transform: scale(1);   opacity: .9; }
  84% { transform: scale(1.8); opacity: 0; }
}

083D 눌림 버튼

버튼 아래에 8px 두께의 딱딱한 그림자를 깔아 물리적인 단추처럼 보이게 하고, 누르는 순간 그 두께만큼 translateY 로 꾹 들어가며 그림자가 사라진다. 손을 떼면 살짝 튕겨 올라오는 것까지 루프에 넣었고 실제 사용에서는 :active 에 두 줄만 있으면 된다. 게임 시작이나 아이들 앱의 큰 버튼에 잘 맞는다.

translateY 눌림box-shadow 두께:active 트리거
$depth: 8px;
.btn {
  box-shadow: 0 $depth 0 darken($color, 22%),
              0 #{$depth + 6px} #{$sp-3} rgba(23, 20, 26, .22);
}
.btn:active { transform: translateY($depth); }
@keyframes press {
  0%, 20%  { transform: translateY(0); }
  35%, 55% { transform: translateY($depth); }
  70%      { transform: translateY(-3px); }
  85%,100% { transform: translateY(0); }
}

09버튼 안 필 로더

파일을 보내는 순간 라벨이 서서히 사라지고 버튼 색이 진한 남색으로 잠긴 뒤, 그 안의 알약 모양 트랙에서 짧은 막대가 왼쪽에서 오른쪽으로 두 번 미끄러진다. 막대는 translateX 로만 움직이고 기다림이 끝나면 라벨이 돌아온다. 실제 사용에서는 제출 함수 동안 is-busy 클래스 하나만 붙이면 된다.

라벨 opacity 스왑translateX 로더 막대$dur-loop 주기
.btn__bar {
  position: absolute;
  top: 0; bottom: 0;
  width: 60px;
  border-radius: $r-pill;
  background: $subject-cream;
  transform: translateX(-60px);
}
@keyframes bar-run {
  0%   { transform: translateX(-60px); }
  100% { transform: translateX(168px); }
}
@keyframes busy-bg {
  0%, 15%  { background: $color; }
  25%, 85% { background: $busy; }
  95%,100% { background: $color; }
}

어디서 깨지는가 — 부드러운 파스텔은 기계가 못 본다

01번을 처음엔 지금과 반대로 만들었다. 배경 그라데이션 자체의 위치를 옮기는 방식(background-position)에 색도 알록달록한 캔디 세 색 대신 부드럽게 스며드는 파스텔을 썼다. 눈으로 보면 분명히 흘렀는데 렌더 프레임을 기계로 재니 강도가 0.0, 움직인 프레임이 0장으로 나왔다. 프레임 사이에 그라데이션이 움직인 거리는 10px인데 그만큼 색이 변하는 폭이 측정 문턱보다 작아 아무 픽셀도 바뀌지 않은 것으로 잡힌 것이다. 부드럽게 스며드는 그라데이션 대신 색 경계가 또렷한 줄무늬로 갈아 움직임도 통째로 translateX 로 옮겨 겨우 측정을 통과했다. 같은 이유로 02번 빛 띠의 이동 거리도 밴드 폭의 몇 배로 잡았다. 아홉 개 파일의 압축 비밀번호는 3vxxw8yh 이고, 틀리면 열리지 않으니 그대로 입력한다. 그라데이션 한 주기(240px)와 이동 거리가 정확히 같아야 루프가 이어지는 것도 이 항목에서 직접 확인했다 — 4px만 어긋나도 한 바퀴마다 무늬가 튄다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce 에서 애니메이션이 꺼지고, 꺼진 뒤에도 각각 무엇인지 알 수 있는 정지 장면을 따로 정해 뒀다. 01은 줄무늬가 보이는 위치에 멈추고, 04는 테두리가 다 그려진 완성 상태로 멈추며, 05는 라벨과 화살표가 함께 보이는 칸에 멈춘다. 09의 막대는 가운데에 멈춰 로딩 중임을 색과 모양으로 알린다. 키보드로 접근할 때는 모든 버튼에 :focus-visible 테두리가 나타난다. 설정을 끄고 켜는 방법은 MDN 의 prefers-reduced-motion 문서 에 있다.

FAQ

자바스크립트를 몰라도 쓸 수 있나요?

네, 이 아홉 개는 전부 자바스크립트 한 줄 없이 CSS(SCSS)만으로 돌아간다. 09번도 실제 사용에서는 버튼에 is-busy 라는 클래스를 붙였다 떼는 것만으로 충분하다. 기본기부터 보고 싶다면 버튼 애니메이션 9종 글 에 리플·호버 같은 첫 걸음들이 있다.

색을 내 브랜드 색으로 바꾸려면 어디를 고치나요?

각 항목 SCSS 맨 위의 $color 하나가 기본 색이다. 01번은 $color 외에 보조색 두 개가 더 있고, 08번은 어두운 두께 색이 darken($color, 22%) 로 자동으로 따라온다. 06번은 아래에 깔리는 색이 $color 라서 위 색을 바꾸려면 배경색 줄을 함께 고친다.

zip 안에는 무엇이 들어있나요?

아홉 개 항목의 index.htmlstyle.scss, 컴파일된 style.css, 그리고 React 프로젝트에 바로 넣을 수 있는 .tsx + .module.scss 아홉 쌍이 들어 있다. 변수는 기본적으로 duration·easing·color 세 개가 props 로 빠져 있고(01번은 보조색 두 개, 09번은 대기색 하나가 추가) 값을 바꾸면 그대로 변형으로 쓸 수 있다. 다른 버튼 소재는 사이트 검색 에서 버튼으로 찾아볼 수 있다.

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

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