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

CSS 버튼 눌림 효과 9종 — 한 줄 복붙으로

CSS 버튼 눌림 효과는 사용자가 버튼·카드·링크를 클릭하거나 터치했을 때 화면이 짧게 반응해 입력이 실제로 먹혔다는 걸 확인시켜 주는 시각적 신호를 말합니다.

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

아홉 개는 인기 순이 아니라 한 번의 탭이 실제로 지나가는 순서로 놓았습니다. 누르자마자 링이 번지고(01), 눌리는 물성이 느껴지고(02), 기기에 따라 반응이 갈리고(03), 오래 누르면 기다림이 끼어들고(04), 목록·카드를 고르고(05~06), 좋아요를 짧게 확인하고(07), 체크박스로 상태를 바꾸고(08), 마지막으로 링크를 눌러 다음 화면으로 넘어갑니다(09). 각 코드는 css 버튼 애니메이션을 돌리는 데 꼭 필요한 부분만 잘라 왔고, 나머지 마크업과 변형 값은 zip 안에 그대로 들어 있습니다. 07에서 짧게 스치고 지나가는 좋아요 확인만 제대로 키우고 싶다면 CSS 하트 애니메이션 아홉 개가 그 한 동작만 다루고, 01의 링을 버튼 둘레를 계속 도는 선으로 바꾸고 싶다면 CSS 테두리 애니메이션 쪽이 이어지는 묶음입니다.

01테두리 링 펄스

css 버튼 효과를 바깥으로 번지게 하기

버튼 안쪽을 채우는 원이 아니라, 버튼 바깥으로 box-shadow 링이 한 번 부풀었다 사라집니다. 이번에 링을 두 겹으로 늘려 서로 다른 속도로 번지다가 단계별로 감쇠하게 해서, 파면이 노이즈처럼 불규칙하게 퍼지는 인상을 만들었습니다.

테두리 링 펄스
새 창에서 열기

01 · 테두리 링 펄스 — 측정 누적면적 4.103% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadowpointerdown0.6초

클릭이 끝나도 끝까지 재생하기

클릭이 끝나도 링은 끝까지 재생돼야 해서 pointerdown에서 클래스를 갈아끼우는 JS 열 줄만 씁니다.

.btn::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  box-shadow: 0 0 0 0 rgba($color, .55);
  opacity: 0;
}
.btn.is-ringing::after {
  animation: ring $duration $easing forwards;
}
@keyframes ring {
  0%   { box-shadow: 0 0 0 0 rgba($color, .55); opacity: 1; }
  100% { box-shadow: 0 0 0 26px rgba($color, 0); opacity: 0; }
}

02눌림 축소

눌리는 물성을 만드는 값

누르는 동안 scale(.96)으로 줄면서 그림자도 함께 얕아지고, 손을 떼면 스프링 없이 짧게 원래대로 돌아옵니다. 여기에 누르는 순간 두 겹 링이 다른 속도로 번졌다 빠르게 감쇠하는 물결이 얹혀, 눌림이 버튼 안에서 끝나지 않고 바깥으로도 번지는 느낌을 줍니다.

눌림 축소 + 그림자 감소
새 창에서 열기

02 · 눌림 축소 + 그림자 감소 — 측정 누적면적 6.326% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scalebox-shadow0.2초

모바일 탭 지연 없애기

touch-action: manipulation을 걸어 모바일 브라우저의 더블탭 확대와 탭 지연을 없앴습니다.

.btn {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;
  touch-action: manipulation;
}
.btn:active {
  transform: scale(.96);
  box-shadow: $shadow-press;
}

03호버 없는 기기 전용 :active

호버 없는 기기만 고르기

@media (hover: none) and (pointer: coarse) 블록 안에만 :active 배경색을 둡니다. 터치 기기에서 누르는 순간엔 두 겹 링이 불규칙하게 번졌다 감쇠하는 물결도 함께 살아납니다.

호버 없는 기기 전용 :active
새 창에서 열기

03 · 호버 없는 기기 전용 :active — 측정 누적면적 8.344% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@media (pointer: coarse)hover: none:active

회색이 눌어붙지 않는 이유

마우스가 있는 기기는 이 블록을 아예 읽지 않아서, 스마트폰에서 탭한 뒤 회색이 눌어붙는 흔한 버그가 여기선 나지 않습니다.

@media (hover: none) and (pointer: coarse) {
  .btn:active {
    background: $stage-ink;
    color: $subject-cream;
  }
}

04길게 누르면 나타나는 지연 반응

600ms 뒤에 오는 반응

누르는 순간엔 아무 일도 없다가 transition-delay로 600ms 뒤에야 카드가 커지고 그림자가 짙어집니다. 반응이 온 순간엔 두 겹 링이 다른 속도로 번지며 터치 지점에서 퍼지듯 감쇠합니다.

길게 누르면 나타나는 지연 반응
새 창에서 열기

04 · 길게 누르면 나타나는 지연 반응 — 측정 누적면적 10.674% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

:active + transition 지연touch-action: none0.6초

진행바 대신 지연 하나

배경이 차오르는 진행바 대신 지연 하나로 짧은 탭과 긴 누름을 가릅니다.

.card {
  transition: transform $duration $easing $hold,
              box-shadow $duration $easing $hold,
              border-color $duration $easing $hold;
  touch-action: none;
}
.card:active {
  transform: scale(1.06);
  box-shadow: $shadow-raised;
  border-color: $color;
}

05리스트 행 탭 하이라이트

눌린 행을 알려 주는 번쩍임

-webkit-tap-highlight-color를 투명 처리한 자리에 탭한 행의 배경색이 짧게 번쩍이고 사라져, 어떤 행을 눌렀는지 확인해 줍니다. 행 안쪽 가장자리로는 두 겹 링이 다른 깊이로 번졌다 감쇠하는 물결이 지나가 터치에 결이 하나 더 얹힙니다.

리스트 행 탭 하이라이트
새 창에서 열기

05 · 리스트 행 탭 하이라이트 — 측정 누적면적 19.752% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

-webkit-tap-highlight-color:active0.2초

손가락 폭에 맞춘 행 높이

행 높이는 44px 이상으로 잡아 손가락 폭에 맞췄습니다.

.list li {
  transition: background $duration $easing;
}
.list li:active {
  background: rgba($color, .18);
  transition: background 0s;
}

06카드 탭 스케일 + 밝기

카드 전체가 눌리는 값

카드를 탭하는 동안 scale(.98)과 filter: brightness(.94)가 함께 걸려 카드 전체가 눌린 듯 보입니다. 가장자리 안쪽으로 두 겹 링이 다른 깊이로 번졌다 빠르게 감쇠해, 카드 바깥으로 나가지 않고 눌림이 카드 안에서 물결칩니다.

카드 탭 스케일 + 밝기
새 창에서 열기

06 · 카드 탭 스케일 + 밝기 — 측정 누적면적 7.167% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scalefilter: brightnesstouch-action

두 번 탭 확대 막기

touch-action: manipulation으로 상품 카드를 빠르게 두 번 누를 때의 확대도 막았습니다.

.card {
  transform: scale(1);
  filter: brightness(1);
  transition: transform $duration $easing, filter $duration $easing;
  touch-action: manipulation;
}
.card:active {
  transform: scale(.98);
  filter: brightness(.94);
}

07탭 확인 지터

140ms 짧은 흔들림

버튼을 탭한 직후 140ms 동안 ±2px 폭으로만 짧게 흔들리며 하트 아이콘이 옅은 색에서 진한 색으로 채워집니다. 흔들림과 함께 붉은 기운의 두 겹 링이 다른 속도로 번졌다 감쇠해 진동이 남긴 여운을 눈으로도 볼 수 있습니다.

탭 확인 지터 (에러 흔들림 아님)
새 창에서 열기

07 · 탭 확인 지터 (에러 흔들림 아님) — 측정 누적면적 1.997% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translate 지터:active0.14초

오류 흔들림과 다른 점

좌우로 세 번 크게 흔드는 오류 표시와는 폭도 목적도 다른, 성공을 확인하는 지터입니다.

.btn:active {
  animation: jitter $duration $easing;
}
.btn:active .btn__icon {
  color: $error;
}
@keyframes jitter {
  0%   { transform: scale(1)    translate(0, 0); }
  25%  { transform: scale(1.18) translate(-2px, 1px); }
  50%  { transform: scale(1.1)  translate(2px, -1px); }
  75%  { transform: scale(1.16) translate(-1px, 1px); }
  100% { transform: scale(1)    translate(0, 0); }
}

08체크박스 탭 피드백

체크선이 그려지는 방식

커스텀 체크박스를 탭하면 체크 아이콘이 stroke-dashoffset으로 그려지는 동시에 박스가 scale(1.08)로 한 번 튑니다. 체크되는 순간 박스 바깥으로 두 겹 링이 다른 속도로 번졌다 감쇠해 선택이 바깥까지 확 번지듯 전해집니다.

체크박스 탭 피드백
새 창에서 열기

08 · 체크박스 탭 피드백 — 측정 누적면적 1.833% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stroke-dashoffsetscale0.2초

숨긴 input 과 형제 선택자

실제 input[type=checkbox]는 숨기고 인접 형제 선택자로 박스와 체크선을 함께 움직입니다.

.cb__input:checked ~ .cb__box {
  border-color: $color;
  background: $color;
  transform: scale(1.08);
}
.cb__input:checked ~ .cb__box .cb__check {
  stroke-dashoffset: 0;
}
.cb__check {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  transition: stroke-dashoffset $duration $easing;
}

09인라인 링크 탭 피드백

링크에도 남는 탭 신호

본문 속 링크를 탭하면 글자 배경에 색 블록이 나타났다 0.14초 만에 사라져, 기본 탭 하이라이트를 지운 자리에도 눌렸다는 신호가 남습니다. 사라지기 전 글자 테두리 안쪽으로 두 겹 물결이 스쳐 지나가 짧은 순간에도 결을 하나 더 만듭니다.

인라인 링크 탭 피드백
새 창에서 열기

09 · 인라인 링크 탭 피드백 — 측정 누적면적 1.469% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

background:active0.14초

핵심 코드

.p__link {
  background: transparent;
  transition: background $duration $easing;
}
.p__link:active {
  background: rgba($color, .32);
  transition: background 0s;
}

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

배경색 알파만 건드리는 효과는 눈에는 보여도 측정에는 안 잡히는 구간이 생기는데, 이는 ai 랜딩페이지 오브 디자인 키트의 빛나는 섹션들을 렌더할 때 똑같이 겪은 지점입니다.

알파 변화를 프레임이 못 잡는다

09 인라인 링크는 반복 구간의 background 알파값을 2초 애니메이션의 0~60% 구간에 걸쳐 linear로 고르게 바꾸도록 처음 만들었습니다. 이 구간이면 알파 변화가 프레임 하나당 11 정도로 잘게 쪼개져, 설정의 픽셀문턱 12(0~255 기준)에도 못 미쳐 판정 하한(강도 15·움직인프레임 6)을 넘기지 못할 계산이었습니다. MDN의 animation-timing-function 문서를 보면 알 수 있듯 linear는 변화를 프레임 수만큼 잘게 쪼개기 때문에, 총 변화량은 그대로 두고 구간을 0~60%에서 0~35%로 좁혀 프레임당 변화 폭을 키웠습니다. 이징을 ease-out으로 바꾸는 방법도 있지만, 눌림 피드백은 손이 닿는 순간 바로 반응해야 해서 linear를 그대로 두고 구간만 손봤습니다. 이렇게 좁혀 실제로 렌더한 뒤 잰 값은 누적면적 1.322%·강도_평균 16.2·움직인프레임 8(24프레임 중)로 하한을 넘겼고, 이 파일의 비밀번호는 e8dzq4mp입니다.

접근성

줄인 움직임에서 남기는 것

아홉 개 전부 prefers-reduced-motion: reduce에서 움직임을 끄되 마지막 상태(눌림·체크됨·하이라이트)는 남겨 반응 자체는 사라지지 않게 했습니다. 탭할 표면 크기도 코드로 맞췄습니다 — 05 리스트 행은 min-height: 44px로, 08 체크박스는 시각적 박스가 26px이라도 label에 min-height: 44px를 줘서 실제 탭 영역을 늘렸습니다. 다만 09처럼 문장 속에 섞인 링크는 WCAG 2.5.8의 예외(문장·블록 텍스트 안 링크)에 해당해 44px을 강제하지 않았습니다. 키보드 사용자를 위해 02·04·06·08에는 :focus-visible 아웃라인을 따로 걸었습니다.

.card:focus-visible {
  outline: 3px solid $color;
  outline-offset: 3px;
}

기본 포커스 링과 이어 볼 글

나머지 다섯(01·03·05·07·09)은 버튼·리스트·링크의 브라우저 기본 포커스 링을 지우지 않고 그대로 씁니다. @media (hover: none) and (pointer: coarse)는 caniuse의 미디어 기능 지원표에서 Chrome·Safari·Firefox 전부 오래전부터 지원하는 걸 확인할 수 있습니다. css 버튼 만들기에서 한 걸음 더 나가, 클릭이 아니라 커서를 따라가는 효과가 궁금하면 커서 이펙트 아홉 가지 글도 있고, 다른 CSS 애니메이션 소재 전부는 CSS 애니메이션 카테고리에 모여 있으며, 이 사이트가 데모를 무엇으로 만드는지는 소개 페이지에 적어 두었습니다.

FAQ

iOS Safari에서도 01의 링이 클릭 즉시 나타납니까?

:active만으로 만들었다면 늦게 나타날 수 있습니다. MDN의 :active 문서에 적힌 대로 iOS Safari는 <body>나 상위 요소에 touchstart 리스너가 하나라도 있어야 :active 스타일을 지연 없이 적용하는데, 01은 애초에 :active가 아니라 pointerdown 이벤트로 클래스를 직접 갈아끼우기 때문에 이 문제를 비켜 갑니다.

03의 터치 전용 스타일은 안드로이드 태블릿처럼 마우스도 붙는 기기에선 어떻게 됩니까?

(hover: none) and (pointer: coarse)는 "주 입력 장치"를 기준으로 판정됩니다. 마우스를 연결해도 태블릿이 스스로를 터치 우선으로 보고하면 여전히 터치용 스타일이 적용되고, 반대로 마우스가 주 입력으로 바뀌면 이 블록은 조용히 꺼집니다.

css 버튼 디자인 아홉 개를 한 폼에 같이 써도 됩니까?

됩니다. 02·06·08처럼 서로 다른 요소(버튼·카드·체크박스)에 걸려 있는 것들은 한 화면에 섞여도 무엇을 눌렀는지 헷갈리지 않습니다. 다만 04처럼 지연이 있는 효과와 02처럼 즉시 반응하는 효과를 같은 버튼에 동시에 걸면 사용자가 어느 쪽 반응을 기다려야 하는지 헷갈리므로 요소당 하나씩만 둡니다. 손을 뗀 뒤 로딩·완료까지 이어서 보여줘야 하는 버튼은 여기 아홉 개가 아니라 누른 다음 상태가 바뀌는 css 버튼 클릭 효과 쪽에서 다룹니다.

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

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