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

북마크 버튼 애니메이션 9가지 — 즐겨찾기 버튼도 같은 원리다

북마크 버튼 애니메이션 아홉 가지를 한 페이지에 모았다. 저장. 한 글자면 충분한 이 말에 아홉 가지 표정이 있다 — 03번은 눌리는 순간 스프링 값이 튀어 오르게 하고, 06번은 저장 수 값이 한 자리씩 쌓이며 배지를 밀어 올리고, 09번은 드래그가 놓이는 거리 값으로 목록에 훅

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

북마크 버튼 애니메이션 9가지 — 값 하나가 저장 느낌을 어떻게 바꾸는가

아홉 개는 손끝 하나의 움직임에서 시작해 화면 여러 곳이 함께 반응하는 순서로 늘어놓았다. 01·02는 마우스 위치·채움 진행률처럼 값 하나만 계산하고, 03·04·05는 누르는 순간의 물리량(스프링 속도·접힘 각도·물 높이)을 매 프레임 적분하며, 06·07은 저장 이후 배지·토스트라는 별도 요소의 타이머를 돌린다. 08·09는 롱프레스·드래그처럼 더 긴 제스처의 거리·시간 값을 문턱값과 비교해 열림·스냅 여부를 가른다.

01포인터 벡터 값이 기울기와 광택 각도를 함께 정하는 북마크 아이콘

마우스가 다가오면 포인터와 버튼 중심 사이 벡터(dx,dy)를 매 프레임 다시 재고, 그 벡터의 크기·각도가 기울기(rotateX·rotateY)와 가장자리를 스치는 광택 각도를 동시에 정한다. GLOW_RADIUS_PX 밖으로 벗어나면 strength 값이 0에 가까워져 기울기·광택이 함께 가라앉는다.

호버로 살짝 기울며 빛나는 북마크 아이콘
새 창에서 열기

01 · 호버로 살짝 기울며 빛나는 북마크 아이콘 — 측정 누적면적 17.316% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TILT_MAX_DEGGLOW_RADIUS_PXapplyVector
  // 값 하나: 포인터 벡터(dx,dy) → 각도. 그 각도가 기울기(rotateX/rotateY)와
  // 광택 sweep 위치(conic-gradient 각도)를 동시에 결정한다.
  function applyVector(dx, dy) {
    var dist = Math.hypot(dx, dy);
    var strength = clamp(1 - dist / GLOW_RADIUS_PX, 0, 1);
    var angleDeg = Math.atan2(dy, dx) * 180 / Math.PI;
    var tiltX = clamp(-dy / GLOW_RADIUS_PX, -1, 1) * TILT_MAX_DEG * strength;
    var tiltY = clamp(dx / GLOW_RADIUS_PX, -1, 1) * TILT_MAX_DEG * strength;
    btn.style.transform = "perspective(600px) rotateX(" + tiltX.toFixed(2) + "deg) rotateY(" + tiltY.toFixed(2) + "deg)";
    glow.style.setProperty("--glow-deg", angleDeg.toFixed(1) + "deg");
    glow.style.setProperty("--glow-strength", strength.toFixed(3));
  }

측정: 누적면적 17.3% · 강도 평균 24.3 · 21/23 프레임에서 움직였다.

02채움 진행률 값이 윤곽선을 안쪽부터 채우는 북마크 버튼

누르기 전에는 리본이 선으로만 보이다가, 클릭하는 순간부터 fill 진행률 값이 0에서 1로 수렴하며 안쪽부터 채운 모양을 만든다. reduced-motion 이면 이 수렴 계산을 건너뛰고 진행률이 목표 값으로 곧장 넘어간다.

윤곽선에서 채움으로 바뀌는 북마크 버튼
새 창에서 열기

02 · 윤곽선에서 채움으로 바뀌는 북마크 버튼 — 측정 누적면적 16.04% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CYCLE_SrendersetSaved
  function render(p) {
    // 값 하나: fill 진행률(p, 0~1) → SVG rect 의 y·height(=clip-path 로 잘려 채움 비율이 된다)
    var y = 24 * (1 - p);
    fillRect.setAttribute("y", y.toFixed(3));
    fillRect.setAttribute("height", (24 - y).toFixed(3));
    panel.style.setProperty("--fill-p", p.toFixed(3));
  }

측정: 누적면적 16.0% · 강도 평균 37.6 · 7/23 프레임에서 움직였다.

03스프링 적분 값이 튕겨 커졌다 되돌아오는 북마크 버튼

누르는 순간 아이콘 크기(pos)가 목표(1)를 넘어 BOOKMARK_SPRING_OVERSHOOT_PERCENT 만큼 더 커졌다가 스프링처럼 튕겨 돌아온다. 이 크기는 가속도를 매 프레임 적분(integrate)해서 얻은 값이라 트랜지션 곡선이 아니라 실제 속도·가속도 계산의 결과다.

스프링으로 튕겨 커지는 북마크 버튼
새 창에서 열기

03 · 스프링으로 튕겨 커지는 북마크 버튼 — 측정 누적면적 9.194% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BOOKMARK_SPRING_OVERSHOOT_PERCENTSPRING_STIFFNESSintegrate
  // 스프링 방정식(강성 SPRING_STIFFNESS · 감쇠 SPRING_DAMPING) 을 매 프레임 적분한다.
  // 눌리는 순간 속도를 한 번 차 넣으면(kick), 스프링이 목표(1)를 넘겼다가 되돌아온다.
  function integrate(dt) {
    var accel = SPRING_STIFFNESS * (1 - pos) - SPRING_DAMPING * vel;
    vel += accel * dt;
    pos += vel * dt;
    if (pos > maxScale) { pos = maxScale; vel *= -0.35; }
    if (pos < 1 && vel < 0 && Math.abs(pos - 1) < 0.001 && Math.abs(vel) < 0.01) { pos = 1; vel = 0; }
  }

측정: 누적면적 9.2% · 강도 평균 28.0 · 12/23 프레임에서 움직였다.

04접힘 각도 값이 그림자까지 진하게 만드는 리본 북마크

누르면 리본 끝이 안쪽으로 접히는 각도(deg)와 살짝 떨어지는 거리(drop)가 같은 진행률 p 하나에서 함께 나오고, p 가 커질수록 그림자의 블러·번짐·불투명도까지 같이 짙어진다. 접힘이 끝나 p 가 0으로 돌아오면 그림자도 원래 옅기로 되돌아온다.

리본이 접히며 떨어지는 북마크 버튼
새 창에서 열기

04 · 리본이 접히며 떨어지는 북마크 버튼 — 측정 누적면적 10.631% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FOLD_MAX_DEGFOLD_DROP_MAX_PXtriggerFold
  // 값 하나: 접힘 진행률 p(0~1, 사인 곡선 한 번의 봉우리) → 접힘 각도(rotateX) + 낙하 거리
  // + 그림자 블러·투명도(높이가 낮아질수록 그림자가 진해진다).
  function render(p) {
    scene.style.setProperty("--tone", p.toFixed(3));
    var deg = p * FOLD_MAX_DEG;
    var drop = p * FOLD_DROP_MAX_PX;
    wrap.style.transform = "perspective(300px) rotateX(" + deg.toFixed(2) + "deg) translateY(" + drop.toFixed(2) + "px)";
    var blur = 3 + p * 10;
    var spread = 6 - p * 4;
    var alpha = 0.22 + p * 0.28;
    shadow.style.boxShadow = "0 " + (2 + p * 6).toFixed(1) + "px " + blur.toFixed(1) + "px " + spread.toFixed(1) + "px rgba(23,20,26," + alpha.toFixed(2) + ")";
    shadow.style.transform = "scaleX(" + (1 - p * 0.18).toFixed(3) + ")";
  }

측정: 누적면적 10.6% · 강도 평균 64.4 · 17/23 프레임에서 움직였다.

05물 높이 값이 사인 물결로 차오르는 북마크 아이콘

저장하면 물 높이 값(level)이 0에서 1로 올라가고, 그 값과 시간 위상(phase)을 매 프레임 다시 넣어 사인 곡선 물결 path(waveD)를 새로 그린다. 속성 트랜지션이 아니라 좌표를 프레임마다 계산해 그리는 방식이라 물결이 흘러가는 것처럼 보인다.

액체가 차오르며 채워지는 북마크 버튼
새 창에서 열기

05 · 액체가 차오르며 채워지는 북마크 버튼 — 측정 누적면적 10.059% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WAVE_AMPLITUDEWAVE_FREQUENCYwaveD
  // 값 하나: 물 높이(level, 0~1) + 시간 위상(phase) → 매 프레임 다시 계산한 사인 물결 path.
  function waveD(lv, phase) {
    var baseY = 24 * (1 - lv);
    var d = "M0," + (baseY + WAVE_AMPLITUDE * Math.sin(phase)).toFixed(2) + " ";
    for (var x = 0; x <= 24; x += 2) {
      var y = baseY + WAVE_AMPLITUDE * Math.sin(x * WAVE_FREQUENCY * 0.26 + phase);
      d += "L" + x + "," + y.toFixed(2) + " ";
    }
    d += "L24,24 L0,24 Z";
    return d;
  }

측정: 누적면적 10.1% · 강도 평균 68.8 · 11/23 프레임에서 움직였다.

06자릿수 지연 값이 한 칸씩 굴려 올리는 저장 수 배지

저장할 때마다 배지 안 두 자릿수가 각각 다른 시점에 구르기 시작하는데, 그 시작 지연은 자릿수 인덱스(오른쪽부터)에 BOOKMARK_COUNT_STAGGER_MS 를 곱한 값이다. 오른쪽 한 자리가 먼저 구르고 왼쪽 자리가 뒤따르는 순서로 값이 하나씩 어긋난다.

저장 수가 한 자리씩 올라가는 배지
새 창에서 열기

06 · 저장 수가 한 자리씩 올라가는 배지 — 측정 누적면적 12.285% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BOOKMARK_COUNT_STAGGER_MSDIGIT_ROLL_MSbeginRoll
  // 값 하나: 자릿수 인덱스(오른쪽부터) × BOOKMARK_COUNT_STAGGER_MS = 그 칸이 구르기 시작하는 지연 시간.
  var columns = [
    { delay: 1 * BOOKMARK_COUNT_STAGGER_MS, start: null, from: "0", to: "0" },
    { delay: 0 * BOOKMARK_COUNT_STAGGER_MS, start: null, from: "9", to: "9" }
  ];

  function pad2(n) { return (n < 10 ? "0" : "") + n; }

  function beginRoll(now) {
    var next = saved ? count + 1 : count - 1;
    var fromStr = pad2(count);
    var toStr = pad2(next);
    count = next;
    for (var i = 0; i < 2; i++) {
      columns[i].from = fromStr[i];
      columns[i].to = toStr[i];
      columns[i].start = now + columns[i].delay;
    }
    status.textContent = saved ? "저장됨" : "저장 전";
    row.classList.add("cb__row--pulse");
    window.setTimeout(function () { row.classList.remove("cb__row--pulse"); }, 320);
  }

측정: 누적면적 12.3% · 강도 평균 69.5 · 12/23 프레임에서 움직였다.

07경과 시간 값이 진입·유지·퇴장을 가르는 저장 토스트

저장을 마치면 화면 아래에서 짧은 문구 띠가 올라오는데, 뜬 뒤 경과 시간을 진입·유지·퇴장 세 구간의 길이(TOAST_ENTER_MS·TOAST_HOLD_MS·나머지)와 견줘 지금이 어느 구간인지, 그 구간 안에서 얼마나 지났는지를 매 프레임 다시 계산한다. 유지 구간에서는 위치·불투명도가 고정값이고 진입·퇴장 구간에서만 값이 바뀐다.

저장을 확인해 주는 토스트 슬라이드
새 창에서 열기

07 · 저장을 확인해 주는 토스트 슬라이드 — 측정 누적면적 10.493% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TOAST_ENTER_MSTOAST_HOLD_MSshowToast
  function showToast(now, msg) {
    toastText.textContent = msg;
    toastStart = now;
  }

  // 값 하나: 토스트가 뜬 뒤 경과 시간(rAF dt 누적) → 진입/유지/퇴장 세 구간 중 어디인지, 그 구간의 진행률.
  function render(now) {
    if (toastStart === null) {
      toast.style.transform = "translateY(120%)";
      toast.style.opacity = "0";
      return;
    }
    var elapsed = now - toastStart;
    var enterMs = reduce ? 1 : TOAST_ENTER_MS;
    var exitMs = reduce ? 1 : TOAST_EXIT_MS;
    var enterEnd = enterMs;
    var holdEnd = enterEnd + TOAST_HOLD_MS;
    var exitEnd = holdEnd + exitMs;
    var y, op;
    if (elapsed < enterEnd) {
      var p = elapsed / enterEnd;
      var eased = 1 - Math.pow(1 - p, 3);
      y = 120 - eased * 120;
      op = eased;

측정: 누적면적 10.5% · 강도 평균 109.0 · 11/23 프레임에서 움직였다.

08롱프레스 임계 값이 목록을 여닫는 컬렉션 팝오버

버튼을 누른 채로 있는 시간이 LONGPRESS_MS 를 넘기면 목록 패널이 열리고, 안에 있는 항목들은 인덱스에 POPOVER_STAGGER_MS 를 곱한 값만큼씩 지연을 두고 차례로 나타난다. 임계값을 넘기지 못하고 손을 떼면 패널은 열리지 않고 짧은 저장 토글만 실행된다.

저장할 컬렉션을 고르는 팝오버
새 창에서 열기

08 · 저장할 컬렉션을 고르는 팝오버 — 측정 누적면적 31.662% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LONGPRESS_MSPOPOVER_STAGGER_MSselectCollection
  // 값 하나: 목록 항목 인덱스 × POPOVER_STAGGER_MS = 그 항목이 열리기 시작하는 지연.
  function renderPanel(now) {
    if (openStart === null && !open) {
      items.forEach(function (el) { el.style.opacity = "0"; el.style.transform = "translateY(6px)"; });
      return;
    }
    var elapsed = openStart === null ? POPOVER_ITEM_MS * 3 : now - openStart;
    items.forEach(function (el, i) {
      var delay = i * POPOVER_STAGGER_MS;
      var raw = (elapsed - delay) / POPOVER_ITEM_MS;
      var p = Math.max(0, Math.min(1, reduce ? 1 : raw));
      var eased = 1 - Math.pow(1 - p, 3);
      el.style.opacity = eased.toFixed(3);
      el.style.transform = "translateY(" + ((1 - eased) * 6).toFixed(2) + "px)";
    });
  }

측정: 누적면적 31.7% · 강도 평균 79.1 · 6/23 프레임에서 움직였다.

09스냅 거리 값이 목록에 붙을지 되튈지 가르는 드래그 북마크

아이콘을 옆 목록까지 끌고 가서 놓으면, 놓은 지점과 목록 자리 사이의 실제 거리를 계산해 BOOKMARK_DRAG_SNAP_PX 보다 가까우면 그 자리로 붙고 아니면 원래 자리로 돌아간다. 돌아가는 동안에는 매 프레임 속도에 DRAG_INERTIA_DECAY 를 곱해 관성이 점점 줄어들게 한다.

드래그해서 읽기 목록에 놓는 북마크
새 창에서 열기

09 · 드래그해서 읽기 목록에 놓는 북마크 — 측정 누적면적 14.492% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BOOKMARK_DRAG_SNAP_PXDRAG_INERTIA_DECAYrelease
  // 값 하나: 놓은 지점과 드롭존 사이 거리 vs BOOKMARK_DRAG_SNAP_PX → 목록에 스냅하거나 원위치로 튕긴다.
  function release() {
    var dx = chipX - dropX, dy = chipY - dropY;
    var dist = Math.sqrt(dx * dx + dy * dy);
    if (dist <= BOOKMARK_DRAG_SNAP_PX) {
      targetX = dropX; targetY = dropY;
      setSaved(true);
    } else {
      targetX = 0; targetY = 0;
      setSaved(false);
    }
  }

측정: 누적면적 14.5% · 강도 평균 43.1 · 19/23 프레임에서 움직였다.

즐겨찾기 버튼과 북마크 버튼은 뭐가 다른가?

북마크 버튼과 즐겨찾기 버튼은 같은 저장 동작을 가리키는 다른 이름일 뿐이다 — 콘텐츠 사이트는 북마크, 쇼핑몰은 위시리스트, 뉴스 앱은 책갈피 버튼이라는 말을 더 많이 쓰지만 아이콘을 누르면 상태 하나(saved)가 뒤집히는 계산 구조는 같다. 하트 좋아요 애니메이션 9종의 값은 저장이 아니라 반응(공감)의 크기를 키우는 데 쓰이는 반면, 여기 아홉 개는 전부 같은 상태를 저장했는지 아닌지 두 값으로만 가른다는 점에서 계산 목적이 갈린다. 07의 저장 완료 문구는 토스트 알림 9종의 등장·유지·퇴장 세 구간 계산을 그대로 가져와 저장이라는 한 가지 이벤트에만 물렸고, 09의 스냅 거리 계산은 드래그 정렬 UI 패턴 9종에서 목록 순서를 바꾸는 데 쓰던 거리 비교를 저장 여부를 가르는 데로 옮겨 왔다.

북마크 버튼 아홉 개를 숫자로 비교하면

NN 이름 누적면적 % 강도 평균 x/23
01 포인터 벡터 값이 기울기와 광택 각도를 함께 정하는 북마크 아이콘 17.3 24.3 21/23
02 채움 진행률 값이 윤곽선을 안쪽부터 채우는 북마크 버튼 16.0 37.6 7/23
03 스프링 적분 값이 튕겨 커졌다 되돌아오는 북마크 버튼 9.2 28.0 12/23
04 접힘 각도 값이 그림자까지 진하게 만드는 리본 북마크 10.6 64.4 17/23
05 물 높이 값이 사인 물결로 차오르는 북마크 아이콘 10.1 68.8 11/23
06 자릿수 지연 값이 한 칸씩 굴려 올리는 저장 수 배지 12.3 69.5 12/23
07 경과 시간 값이 진입·유지·퇴장을 가르는 저장 토스트 10.5 109.0 11/23
08 롱프레스 임계 값이 목록을 여닫는 컬렉션 팝오버 31.7 79.1 6/23
09 스냅 거리 값이 목록에 붙을지 되튈지 가르는 드래그 북마크 14.5 43.1 19/23

면적이 가장 큰 것은 08이다 — 패널 전체가 새로 나타나는 화면이라 아이콘 하나만 움직이는 나머지보다 넓게 반응한다. 02·07·08은 24프레임 중 절반도 안 되는 구간에서만 값이 바뀌는데, 이 셋은 모두 클릭 한 번 뒤에만 짧게 반응하고 나머지 시간은 값이 그대로 멈춰 있는 이벤트형 계산이기 때문이다. 강도 평균은 07이 가장 높다 — 토스트가 짧은 시간 안에 위치·불투명도를 크게 바꾸는 구간이 몰려 있어서다.

값 하나 숫자만 바꿔도 북마크 버튼 반응이 갈라지는 변형 세 가지

항목 바꾸는 값 줄이면 늘리면
03 BOOKMARK_SPRING_OVERSHOOT_PERCENT 12 → 38 튕김 폭이 짧아져 크기 변화가 거의 트랜지션처럼 차분해진다 넘치는 폭이 커져 한 번 크게 부풀었다 가라앉는 동작이 뚜렷해진다
06 BOOKMARK_COUNT_STAGGER_MS 40 → 160 두 자리가 거의 같이 바뀌어 굴러가는 느낌이 옅어진다 오른쪽·왼쪽 자리가 시차를 두고 따로 굴러가는 게 눈에 보인다
09 BOOKMARK_DRAG_SNAP_PX 24 → 96 자리 바로 위까지 정확히 끌고 가야만 붙는다 자리에서 멀리 떨어뜨려도 저절로 끌려가 붙는다

위 세 값을 실제로 어떻게 바꿔 넣었는지 원본 그대로 보고 싶다면, 압축을 열 때 w36j826f 을 넣어 변형 전후 코드를 나란히 대조해 볼 수 있다.

북마크 버튼을 실제로 어디에 놓으면 좋은가

북마크 아이콘 만들기를 검색해 여기까지 왔다면 01·02의 기본형만 옮겨도 충분하다 — 호버 반응이나 채움 모양 정도만 있어도 대부분의 카드 목록에 자연스럽게 어울린다. 글 목록 카드에는 01이, 제품 위시리스트에는 02나 05처럼 채워지는 모양이 어울리고, 댓글·짧은 목록의 즉각적인 반응에는 03이 맞는다. 저장 수를 사용자에게 보여줘야 하는 화면에는 06을, 저장 직후 확인이 필요한 모바일 화면에는 07을 붙인다. 폴더별로 나눠 저장해야 하는 레시피·기사 사이트는 08을, 순서를 직접 옮겨 정리하는 북마크 관리 화면은 09를 그대로 쓸 수 있다.

어디서 깨지는가 — 동작 줄임 설정·터치 오탐·중복 클릭

동작 줄임(prefers-reduced-motion)을 켜면 아홉 개 모두 루프 계산을 건너뛰고 최종 상태 값으로 바로 넘어간다 — 03은 스프링 적분 대신 pos 를 1로 고정하고, 06은 컬럼 지연을 전부 0으로 만들어 자릿수가 한 번에 바뀐다(MDN, prefers-reduced-motion). 08은 누른 시간만으로 롱프레스를 판정하고 pointermove·pointercancel 을 듣지 않는다 — 손가락이 눌린 채로 화면을 살짝 움직여도 그 동작이 그대로 눌린 시간으로 잡혀 스크롤 대신 목록 패널이 먼저 열릴 수 있다(MDN, Pointer Events). 09는 반대로 드래그 칩에 touch-action:none 을 걸어 두어 이 문제를 미리 막아 뒀다. 잠금 값이 없는 항목은 연속 클릭이 그대로 쌓인다 — 03을 두 번 빠르게 누르면 속도 값이 두 번 더해져 더 크게 튕기고, 06을 구르는 도중에 다시 누르면 진행 중이던 컬럼 지연이 새 값으로 덮여 자릿수가 중간에 다시 시작한다. 실제 코드에서 이 부분을 어떻게 짰는지는 아홉 개 js 를 열어 직접 대조해 보면 가장 빠르다.

접근성 — reduced-motion·aria-pressed·키보드로 북마크 버튼 다루기

아홉 개 버튼 전부 aria-pressed 값을 저장 여부에 맞춰 true·false 로 바꾸고, 상태 문구는 aria-live="polite" 영역에 다시 쓰여 스크린 리더가 "저장됨" 같은 변화를 읽어 준다. 버튼은 모두 네이티브 button 요소라 Tab 으로 포커스를 옮기고 Enter·Space 로 그대로 누를 수 있다 — 별도의 keydown 처리를 얹지 않아도 기본 동작이 클릭을 대신한다. 08의 컬렉션 팝오버는 롱프레스가 포인터 전용이라 여기에 별도 키보드 대안을 얹어 뒀다 — 트리거에 aria-haspopup·aria-expanded 를 붙이고 목록에 role="menu"·항목마다 role="menuitem" 을 달아, Enter·Space 는 짧은 저장 토글을 그대로 실행하고 ArrowDown 또는 Shift+Enter 는 목록 패널을 열며 첫 항목으로 포커스를 옮긴다. 패널이 열린 뒤에는 ArrowUp·ArrowDown 으로 항목 사이를 오가고 Enter 로 그 항목을 고르며, Escape 는 패널을 닫고 포커스를 트리거 버튼으로 되돌린다.

북마크 버튼을 실무에 붙이기 전, 자주 헷갈리는 세 가지

북마크 아이콘 표시가 저장 전후로 정말 바뀌는지 어떻게 확인하나?

버튼의 aria-pressed 속성과 아이콘에 붙는 class(is-saved 류)가 클릭마다 같이 바뀐다. 요소를 열어 두고 클릭해 보면 속성 값이 true·false 로 뒤집히는 게 그대로 보인다.

아홉 개 전부 같은 아이콘 모양(SVG)을 써야 하나?

아니다. 여기서 계산이 붙는 건 아이콘을 감싼 버튼·패널의 움직임이지 아이콘 모양 자체가 아니다. 리본이든 별이든 원하는 SVG path 로 바꿔도 tiltX·fill 진행률 같은 값 계산은 그대로 옮겨 쓸 수 있다.

저장 수 같은 서버 값이 없는 페이지에도 06을 쓸 수 있나?

쓸 수 있다. 06은 count 변수 하나만 늘리고 줄이는 로컬 계산이라 서버 응답 없이도 그대로 동작한다. 실제로 붙일 때는 저장·해제 응답을 받은 뒤 그 값으로 count 를 갱신하도록 한 줄만 바꾸면 된다.

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

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