GODRICH

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

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

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

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

01테두리 링 펄스

버튼 안쪽을 채우는 원이 아니라, 버튼 바깥으로 box-shadow 링이 한 번 부풀었다 사라집니다. 클릭이 끝나도 링은 끝까지 재생돼야 해서 pointerdown에서 클래스를 갈아끼우는 JS 열 줄만 씁니다.

box-shadowpointerdown0.6초
.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)으로 줄면서 그림자도 함께 얕아지고, 손을 떼면 스프링 없이 짧게 원래대로 돌아옵니다. touch-action: manipulation을 걸어 모바일 브라우저의 더블탭 확대와 탭 지연을 없앴습니다.

scalebox-shadow0.2초
.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 배경색을 둡니다. 마우스가 있는 기기는 이 블록을 아예 읽지 않아서, 스마트폰에서 탭한 뒤 회색이 눌어붙는 흔한 버그가 여기선 나지 않습니다.

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

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

누르는 순간엔 아무 일도 없다가 transition-delay로 600ms 뒤에야 카드가 커지고 그림자가 짙어집니다. 배경이 차오르는 진행바 대신 지연 하나로 짧은 탭과 긴 누름을 가릅니다.

: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를 투명 처리한 자리에 탭한 행의 배경색이 짧게 번쩍이고 사라져, 어떤 행을 눌렀는지 확인해 줍니다. 행 높이는 44px 이상으로 잡아 손가락 폭에 맞췄습니다.

-webkit-tap-highlight-color:active0.2초
.list li {
  transition: background $duration $easing;
}
.list li:active {
  background: rgba($color, .18);
  transition: background 0s;
}

06카드 탭 스케일 + 밝기

카드를 탭하는 동안 scale(.98)filter: brightness(.94)가 함께 걸려 카드 전체가 눌린 듯 보입니다. touch-action: manipulation으로 상품 카드를 빠르게 두 번 누를 때의 확대도 막았습니다.

scalefilter: brightnesstouch-action
.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 동안 ±2px 폭으로만 짧게 흔들리며 하트 아이콘이 옅은 색에서 진한 색으로 채워집니다. 좌우로 세 번 크게 흔드는 오류 표시와는 폭도 목적도 다른, 성공을 확인하는 지터입니다.

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)로 한 번 튑니다. 실제 input[type=checkbox]는 숨기고 인접 형제 선택자로 박스와 체크선을 함께 움직입니다.

stroke-dashoffsetscale0.2초
.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초 만에 사라져, 기본 탭 하이라이트를 지운 자리에도 눌렸다는 신호가 남습니다.

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

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

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이라도 labelmin-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 애니메이션 카테고리에 모여 있으며, 이 사이트가 데모를 무엇으로 만드는지는 소개 페이지에 적어 두었습니다.

FAQ

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

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

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

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

아홉 개를 한 폼에 같이 써도 됩니까?

됩니다. 02·06·08처럼 서로 다른 요소(버튼·카드·체크박스)에 걸려 있는 것들은 한 화면에 섞여도 무엇을 눌렀는지 헷갈리지 않습니다. 다만 04처럼 지연이 있는 효과와 02처럼 즉시 반응하는 효과를 같은 버튼에 동시에 걸면 사용자가 어느 쪽 반응을 기다려야 하는지 헷갈리므로 요소당 하나씩만 둡니다.

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

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