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

css download button 9가지 — 눌리고 차오르고 파일이 날아가는 버튼

css download button 을 라벨만 바꾸는 상태 전환으로 만들면 화살표는 뻣뻣하고 진행은 눈금처럼 끊기고 파일이 어디 갔는지 모른다 — 01번·02번·03번이 계산으로 푼다. 받기 버튼 하나를 누름에서 완료·실패까지 움직임으로 보여 주는 UI 다.

다운로드 버튼 아홉 장면을 화면에서 바로 본다

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

css download button 9가지 — 스프링·진행 곡선·파일 경로를 계산해야 받기가 느껴진다

아홉 장면은 받기 하나가 흘러가는 순서대로 놓았다. 01 은 눌렸을 때 가라앉는 화살표, 02 는 진행률 곡선으로 차오르는 링, 03 은 트레이로 날아가는 파일이고, 04 는 받은 크기를 세어 올리는 칩, 05 는 오르는 액체 수위, 06 은 화살표가 체크로 바뀌는 완료다. 07 은 형식을 고르는 분리 버튼, 08 은 실패 뒤 흔들리고 다시 시도하는 버튼, 09 는 커서 쪽으로 끌리며 번쩍이는 첫인상 버튼이다.

이런 화면에서 쓴다

자료실이나 제품 소개 페이지의 받기 버튼처럼 한 번 눌러 파일을 가져가게 하는 자리에서 쓴다. 용량이 큰 자료를 받게 하는 화면은 02·04 의 진행 표시가, 받은 파일을 여러 개 모아 두는 화면은 03 의 트레이가, 형식이 여러 가지인 문서는 07 의 분리 버튼이 먼저 필요하다. 어느 쪽이든 받기의 상태를 숫자 하나로 정리하고 그 숫자에서 모양을 계산하는 방식은 같다.

이웃 편과 무엇이 다른가

css 버튼 클릭 효과 9종 — 누른 다음이 진짜다는 누른 뒤 상태가 어떻게 바뀌는지를, 다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식은 받는 중인 파일이 목록에 쌓이는 화면을 다룬다. 이 편은 버튼 하나의 눌림·진행·도착·완료·실패가 각각 어떤 계산으로 움직이는지를 아홉 장면으로 다룬다.

기법별 코드로 보는 다운로드 버튼

아홉 장면은 모두 진짜 <button> 이고 움직임은 CSS 전환이 아니라 JS 가 프레임마다 계산한 값으로 칠한다. 입력이 없으면 .is-demo 가 켜져 있는 동안 2초 주기로 같은 동작이 되풀이되고, 첫 입력이 오면 그 자리에서 꺼지며 같은 계산이 입력을 받는다. 라이브러리와 CDN 은 쓰지 않았고 아래 코드는 소스에서 연속 구간만 그대로 가져왔다.

01눌리면 가라앉는 화살표

누르면 화살표가 SINK_PX 10 만큼 내려앉고 포인터가 눌린 가로 위치 쪽으로 최대 14도 기울며, 두 값 모두 스프링 식을 8ms 이하 부분 스텝으로 적분해 얻는다. SPRING_K 120 과 SPRING_DAMP 9 에서 감쇠비는 약 0.41 이라 놓으면 눌린 깊이의 약 24% 를 지나쳤다가 돌아오는, 받기 버튼의 기본 눌림으로 쓸 만한 세기다. 측정은 누적면적 13.3%, 강도 평균 72.3, 움직인 프레임 23/23 이다.

눌리면 가라앉는 화살표
새 창에서 열기

01 · 눌리면 가라앉는 화살표 — 측정 누적면적 13.29% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SINK_PXSPRING_KSPRING_DAMP
  function integrate(dt, g) {
    var n = Math.ceil(dt / 0.008);
    var h = dt / n;
    for (var i = 0; i < n; i += 1) {
      vy += (-SPRING_K * (y - g.depth) - SPRING_DAMP * vy) * h;
      y += vy * h;
      vt += (-SPRING_K * (tilt - g.lean * TILT_MAX) - SPRING_DAMP * vt) * h;
      tilt += vt * h;
    }
  }

  function render() {
    var p = clamp(y / SINK_PX, -0.3, 1);
    icon.style.transform = 'translateY(' + y.toFixed(2) + 'px) rotate(' + tilt.toFixed(2) + 'deg)';
    btn.style.transform = 'translateY(' + (y * 0.3).toFixed(2) + 'px)';
    btn.style.setProperty('--p', p.toFixed(3));
    yEl.textContent = y.toFixed(1);
    tiltEl.textContent = tilt.toFixed(1);
  }

02진행률 곡선으로 차오르는 테두리 링

굵은 링은 CURVE_POW 2 의 곡선 1 - (1 - u)^CURVE_POW 를 따르고 안쪽 가는 링은 균일 진행 u 를 그대로 따라, 둘의 차이 LEAD 가 u 0.5 에서 최대 25점까지 벌어진다. 대용량 파일 받기 안내처럼 시작이 빨라 보여야 하는 자리에 쓰고, 링과 버튼 안 채움이 같은 진행률을 읽으며 aria-valuenow 가 매 프레임 갱신된다. 측정은 누적면적 11.1%, 강도 평균 117.5, 움직인 프레임 19/23 이다.

진행률 곡선으로 차오르는 테두리 링
새 창에서 열기

02 · 진행률 곡선으로 차오르는 테두리 링 — 측정 누적면적 11.07% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CURVE_POWRING_RRUN_MS
  function render(now) {
    var s = startOf(now);
    var u = s === null ? 0 : Math.max(0, Math.min(1, (now - s) / RUN_MS));
    if (reduce && runStart === 'done') u = 1;
    var p = ringCurve(u);
    var pct = Math.round(p * 100);
    ring.style.strokeDashoffset = String((100 - p * 100).toFixed(2));
    ghost.style.strokeDashoffset = String((100 - u * 100).toFixed(2));
    ring.style.opacity = p < 0.004 ? '0' : '1';
    ghost.style.opacity = u < 0.004 ? '0' : '1';
    btn.style.setProperty('--f', p.toFixed(3));
    pctEl.textContent = pct + '%';
    leadEl.textContent = String(Math.round((p - u) * 100));
    labelEl.textContent = u >= 1 ? '완료' : (u > 0 ? '받는 중' : '내려받기');
    bar.setAttribute('aria-valuenow', String(pct));
    bar.setAttribute('aria-valuetext', pct + '%');
  }

03트레이로 날아가 들어가는 파일

파일 아이콘이 FLY_MS 700ms 동안 2차 베지어 곡선을 따라 가며 최대 ARC_HEIGHT 56px 휘고, 회전하고 작아지다 트레이 가까이에서 사라진다. 도착하면 트레이가 TRAY_BOUNCE 14px 눌렸다가 감쇠비 약 0.37 의 스프링으로 몇 번 출렁이며 돌아오고 개수가 하나 늘어, 받은 파일이 어디로 갔는지 보여 줄 때 쓴다. 측정은 누적면적 11.8%, 강도 평균 101.2, 움직인 프레임 21/23 이다.

트레이로 날아가 들어가는 파일
새 창에서 열기

03 · 트레이로 날아가 들어가는 파일 — 측정 누적면적 11.791% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLY_MSARC_HEIGHTTRAY_BOUNCE
  function renderFile(now) {
    if (flightStart === null) { file.style.opacity = '0'; return; }
    var t = Math.min(1, (now - flightStart) / FLY_MS);
    var e = ease(t);
    var g = geometry();
    var cx = g.x + 2 * ARC_HEIGHT;
    var cy = (g.y0 + g.y2) / 2;
    var x = (1 - e) * (1 - e) * g.x + 2 * (1 - e) * e * cx + e * e * g.x;
    var y = (1 - e) * (1 - e) * g.y0 + 2 * (1 - e) * e * cy + e * e * g.y2;
    var rot = 28 * Math.sin(Math.PI * e);
    var sc = 1 - 0.45 * e;
    file.style.opacity = t > 0.9 ? String((1 - t) * 10) : '1';
    file.style.transform = 'translate(' + (x - 14).toFixed(1) + 'px,' + (y - 14).toFixed(1) + 'px) rotate(' + rot.toFixed(1) + 'deg) scale(' + sc.toFixed(3) + ')';
  }

  function renderTray() {
    tray.style.transform = 'translateY(' + ty.toFixed(2) + 'px)';
    tray.style.setProperty('--h', Math.max(0, Math.min(1, ty / TRAY_BOUNCE)).toFixed(3));
    countEl.textContent = String(count);
  }

04받은 크기를 세어 올리는 칩

받은 크기는 TARGET_MB 48 에 COUNT_EASE 3 제곱의 감속 곡선을 곱해 올라가고, 남은 시간은 남은 양을 지금까지의 평균 속도로 나눠 구한다. 48MB 와 2분은 시연용 상수이고 남은 시간은 시작 직후 약 40초에서 출발해 0:00 으로 줄어, 파일 크기와 남은 시간을 함께 적는 자리에 틀로 쓴다. 측정은 누적면적 18.6%, 강도 평균 80.9, 움직인 프레임 19/23 이다.

받은 크기를 세어 올리는 칩
새 창에서 열기

04 · 받은 크기를 세어 올리는 칩 — 측정 누적면적 18.608% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COUNT_EASETARGET_MBETA_MIN
  function render(now) {
    var s = startOf(now);
    var u = s === null ? 0 : Math.max(0, Math.min(1, (now - s) / RUN_MS));
    if (reduce && finished) u = 1;
    var got = TARGET_MB * (1 - Math.pow(1 - u, COUNT_EASE));
    var eta = '--:--';
    if (u > 0) {
      var elapsedSec = Math.max(u, 0.004) * ETA_MIN * 60;
      var avg = Math.max(got, 0.01) / elapsedSec;
      var left = Math.round((TARGET_MB - got) / avg);
      eta = Math.floor(left / 60) + ':' + two(left % 60);
    }
    root.style.setProperty('--f', (got / TARGET_MB).toFixed(3));
    gotEl.textContent = got.toFixed(1);
    etaEl.textContent = eta;
    labelEl.textContent = u >= 1 ? '완료' : (u > 0 ? '받는 중' : '내려받기');
    if (u >= 1 && !announced) { announced = true; statusEl.textContent = '받기 완료'; }
    if (u < 1 && announced) { announced = false; statusEl.textContent = ''; }
  }

05진행률을 따라 오르는 액체 수위

수면은 48구간(점 49개)을 두 사인파의 합으로 이은 SVG 경로이고, 진폭 amp 는 수위가 찰수록 줄어 가득 차면 잔잔해진다. 수위는 목표 진행률을 LEVEL_LERP 6 의 지수 보간으로 따라가며, 진행 상태를 그림으로 보여 주는 CTA 에 쓴다. 측정은 누적면적 12.0%, 강도 평균 130.8, 움직인 프레임 23/23 이다.

진행률을 따라 오르는 액체 수위
새 창에서 열기

05 · 진행률을 따라 오르는 액체 수위 — 측정 누적면적 12.04% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WAVE_AMPWAVE_FREQLEVEL_LERP
  function draw(phase) {
    var reach = WAVE_AMP * 1.5;
    var base = -reach + (H + 2 * reach) * (1 - level);
    var amp = WAVE_AMP * (1 - 0.7 * level);
    var d = 'M0 ' + H;
    for (var i = 0; i <= POINTS; i += 1) {
      var x = (i / POINTS) * W;
      var a = 2 * Math.PI * WAVE_FREQ * (x / W);
      var y = base + amp * (Math.sin(a + phase) + 0.5 * Math.sin(2 * a - 1.3 * phase));
      d += 'L' + x.toFixed(1) + ' ' + y.toFixed(2);
    }
    d += 'L' + W + ' ' + H + 'Z';
    pathEl.setAttribute('d', d);
    var pct = Math.round(level * 100);
    pctEl.textContent = pct + '%';
    labelEl.textContent = level > 0.995 ? '완료' : (level > 0.01 ? '받는 중' : '내려받기');
    levelEl.textContent = level.toFixed(2);
    ampEl.textContent = amp.toFixed(1);
  }

06선 길이로 화살표가 체크로 바뀌는 완료

화살표와 체크 두 경로의 stroke-dashoffset 을 진행값 p 하나로 계산해, p 0.5 까지 화살표 선이 줄고 0.4 부터 체크 선이 그어지며 라벨은 p 0.5 에서 바뀐다. 버튼 폭은 보이지 않는 자 요소의 offsetWidth 로 라벨 폭을 재서 WIDTH_LERP 10 으로 옮겨 가므로, 받기 완료를 확인시키는 자리에 맞다. 측정은 누적면적 11.6%, 강도 평균 50.8, 움직인 프레임 18/23 이다.

선 길이로 화살표가 체크로 바뀌는 완료
새 창에서 열기

06 · 선 길이로 화살표가 체크로 바뀌는 완료 — 측정 누적면적 11.569% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STROKE_LENCHECK_MSWIDTH_LERP
  function render(want) {
    var shrink = clamp(p / 0.5, 0, 1);
    var draw = clamp((p - 0.4) / 0.6, 0, 1);
    arrow.style.strokeDashoffset = String((STROKE_LEN * shrink).toFixed(2));
    check.style.strokeDashoffset = String((STROKE_LEN * (1 - draw)).toFixed(2));
    label.textContent = p >= 0.5 ? '완료' : '내려받기';
    label.style.opacity = String(Math.abs(2 * p - 1).toFixed(2));
    btn.style.setProperty('--p', p.toFixed(3));
    btn.style.width = width.toFixed(1) + 'px';
    pEl.textContent = p.toFixed(2);
    wEl.textContent = String(Math.round(width));
    if (p >= 1 && !announced) { announced = true; statusEl.textContent = '받기 완료'; }
    if (p <= 0 && announced) { announced = false; statusEl.textContent = ''; }
  }

07형식을 고르는 분리 버튼

FORMATS 세 칸이 MENU_STAGGER 70ms 씩 늦춰 easeOutBack 으로 펼쳐지고, 고른 형식은 높이 24px 라벨 창에서 옛 글자가 LABEL_RISE 24px 위로 나가고 새 글자가 아래에서 올라온다. 토글은 ArrowDown 으로 열고 화살표 키로 칸을 옮기며 Escape 로 닫아, PDF·PNG 처럼 형식이 갈리는 받기에 쓴다. 측정은 누적면적 17.7%, 강도 평균 90.6, 움직인 프레임 19/23 이다.

형식을 고르는 분리 버튼
새 창에서 열기

07 · 형식을 고르는 분리 버튼 — 측정 누적면적 17.706% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MENU_STAGGERFORMATSLABEL_RISE
  function render() {
    var s = open * OPEN_MS;
    var visible = open > 0.001 || wantOpen;
    for (var i = 0; i < items.length; i += 1) {
      var q = outBack(clamp((s - i * MENU_STAGGER) / ITEM_MS, 0, 1));
      var o = clamp(q, 0, 1);
      items[i].style.opacity = o.toFixed(3);
      items[i].style.transform = 'translateY(' + ((1 - q) * -16).toFixed(2) + 'px) scale(' + (0.8 + 0.2 * q).toFixed(3) + ')';
      items[i].style.visibility = visible ? 'visible' : 'hidden';
      items[i].setAttribute('tabindex', wantOpen ? '0' : '-1');
      items[i].classList.toggle('is-picked', i === wantFmt);
    }
    menu.setAttribute('aria-hidden', wantOpen ? 'false' : 'true');
    toggle.setAttribute('aria-expanded', wantOpen ? 'true' : 'false');
    toggle.style.setProperty('--o', open.toFixed(3));
    root.style.setProperty('--k', (1 - rise).toFixed(3));
    oldEl.style.transform = 'translateY(' + (-LABEL_RISE * outCubic(rise)).toFixed(2) + 'px)';
    newEl.style.transform = 'translateY(' + (LABEL_RISE * (1 - outBack(rise))).toFixed(2) + 'px)';
    oldEl.style.opacity = String((1 - rise).toFixed(2));
    textEl.textContent = FORMATS[wantFmt];
  }

08실패하고 감쇠하며 다시 시도하는 버튼

실패하면 버튼 전체가 SHAKE_AMP 14px 에서 시작해 초당 SHAKE_DECAY 5 의 비율로 줄며 4.5Hz 로 흔들리고, REWIND_MS 500ms 동안 링이 처음으로 되감긴 뒤 라벨이 다시 시도로 바뀐다. 실제 입력은 첫 시도가 실패하고 두 번째가 끝까지 가도록 코드가 정해 둔 시연이라, 끊긴 받기를 복구하는 화면의 틀로만 쓴다. 측정은 누적면적 10.9%, 강도 평균 135.5, 움직인 프레임 23/23 이다.

실패하고 감쇠하며 다시 시도하는 버튼
새 창에서 열기

08 · 실패하고 감쇠하며 다시 시도하는 버튼 — 측정 누적면적 10.896% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SHAKE_AMPSHAKE_DECAYREWIND_MS
    var t = now - s;
    if (t < RUN_MS) {
      var u = outQuad(t / RUN_MS);
      return { p: res === 'fail' ? FAIL_AT * u : u, x: 0, e: 0, label: '받는 중', glyph: ARROW, note: '' };
    }
    if (res === 'ok') return { p: 1, x: 0, e: 0, label: '완료', glyph: ARROW, note: '받기 완료' };
    var tau = (t - RUN_MS) / 1000;
    var rw = clamp((t - RUN_MS - REWIND_DELAY) / REWIND_MS, 0, 1);
    return {
      p: FAIL_AT * (1 - inOut(rw)),
      x: SHAKE_AMP * Math.exp(-SHAKE_DECAY * tau) * Math.sin(2 * Math.PI * SHAKE_HZ * tau),
      e: 1,
      label: rw >= 1 ? '다시 시도' : '실패',
      glyph: BANG,
      note: '받기가 끊겼어요'
    };

09커서 쪽으로 끌리며 번쩍이는 버튼

버튼은 커서와 중심의 거리에 PULL_RATE 0.12 를 곱한 만큼 최대 LEAN_MAX 10px 까지 끌려가고, 폭 SHEEN_W 64px 의 광택 띠는 커서의 가로 위치를 지수 보간으로 쫓는다. 커서를 감지하는 범위는 버튼이 아니라 그 둘레 최대 280x144 영역이라, 랜딩 페이지 주 CTA 의 첫인상에 쓴다. 측정은 누적면적 18.2%, 강도 평균 82.3, 움직인 프레임 23/23 이다.

커서 쪽으로 끌리며 번쩍이는 버튼
새 창에서 열기

09 · 커서 쪽으로 끌리며 번쩍이는 버튼 — 측정 누적면적 18.191% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PULL_RATESHEEN_WLEAN_MAX
  function goal(now) {
    var w = btn.offsetWidth;
    var h = btn.offsetHeight;
    var d = null;
    if (root.classList.contains('is-demo') && now >= START_MS) {
      var a = 2 * Math.PI * ((now - START_MS) % CYCLE_MS) / CYCLE_MS;
      d = { x: ORBIT_X * Math.cos(a), y: ORBIT_Y * Math.sin(a) };
    } else if (cursor) {
      d = cursor;
    }
    if (kbFocus && !d) return { lx: 0, ly: 0, sx: w / 2, a: 1, gx: 50, gy: 50 };
    if (!d) return { lx: 0, ly: 0, sx: -SHEEN_W * 2, a: 0, gx: 50, gy: 50 };
    return {
      lx: clamp(d.x * PULL_RATE, -LEAN_MAX, LEAN_MAX),
      ly: clamp(d.y * PULL_RATE, -LEAN_MAX, LEAN_MAX),
      sx: clamp(d.x + w / 2, -SHEEN_W, w + SHEEN_W),
      a: 1,
      gx: clamp((d.x + w / 2) / w * 100, -20, 120),
      gy: clamp((d.y + h / 2) / h * 100, -40, 140)
    };
  }

값이 장면을 바꾸는 곳 — 스프링 세기·곡선 지수·진폭

상수 셋이 눌림의 출렁임, 진행의 앞섬, 수면의 흔들림을 거의 다 정한다. css download button animation 을 다룰 때 곡선이나 스프링의 식 자체보다 이 값 하나를 바꿔 보는 쪽이 장면의 성격을 훨씬 빨리 바꾼다. 아래 수치는 각 항목을 두 초 동안 렌더해 24장에서 뽑았고 움직임 분모는 프레임 사이 간격인 23 이다.

항목 누적면적 % 강도 평균 움직인 프레임
01 가라앉는 화살표 13.3% 72.3 23/23
02 진행 곡선 링 11.1% 117.5 19/23
03 날아가는 파일 11.8% 101.2 21/23
04 크기 세기 칩 18.6% 80.9 19/23
05 액체 수위 12.0% 130.8 23/23
06 체크 선 모핑 11.6% 50.8 18/23
07 분리 형식 메뉴 17.7% 90.6 19/23
08 실패 흔들림 10.9% 135.5 23/23
09 자석 광택 18.2% 82.3 23/23

누적면적이 10% 아래인 항목은 없다. 움직인 프레임이 23 미만인 항목은 02·03·04·06·07 이고, 이 글은 그 이유를 소스에서 따로 확인하지 않았으므로 측정값만 적는다.

스프링 세기가 오버슛을 정한다

01 의 감쇠비는 SPRING_DAMP 9 를 2 와 SPRING_K 의 제곱근 곱으로 나눈 값이다. 120 에서는 약 0.41 이고 눌린 깊이 10px 의 약 24% 인 2.4px 를 지나친다. 같은 감쇠에서 SPRING_K 만 240 으로 올리면 감쇠비가 약 0.29 로 내려가 지나치는 폭이 약 39% 로 커진다.

곡선 지수가 앞섬을 정한다

02 의 LEAD 는 곡선 진행에서 균일 진행을 뺀 값이라 지수 2 에서는 u 0.5 일 때 최대 25점이다. CURVE_POW 를 4 로 올리면 u 약 0.37 에서 약 47점까지 벌어지고, 곡선이 균일 진행보다 앞서 나갈 뿐 뒤처지지는 않는다.

진폭이 수면의 흔들림을 정한다

05 는 amp = WAVE_AMP (1 - 0.7 level) 라 수위 0.5 에서 WAVE_AMP 6 이면 진폭이 약 3.9px 이다. 12 로 올리면 같은 수위에서 약 7.8px 가 되고, 수위가 찰수록 진폭이 줄어드는 관계는 그대로다.

어디서 깨지는가 — 역행하는 진행률·좁은 버튼·키보드만 쓸 때

진행률이 거꾸로 가면 수위도 빠진다

02·04 는 시간에서 진행률을 계산하므로 거꾸로 가는 일이 없다. 05 는 목표 진행률이 내려가면 수위가 LEVEL_LERP 로 빠지고 퍼센트 글자도 함께 줄어드니, 시연이 주기 끝에 0 으로 돌아갈 때 수위가 빠지는 모습이 그것이다. 진짜 진행률을 넣을 때는 입력이 단조 증가인지부터 확인한다.

라벨이 길면 폭이 모자란다

06 은 폭을 라벨 실측 너비로 다시 잡지만 max-width: 100% 와 넘침 숨김이 걸려 있어, 영어나 일본어처럼 라벨이 긴 문구로 바꾸면 끝이 잘릴 수 있다. 07 은 형식 칸 셋이 같은 비율이라 폭이 260px 아래로 줄면 글자가 좁아진다.

키보드만 쓰면 기울기도 끌림도 없다

01 은 키로 누르면 기울기 목표를 0 으로 두므로 포인터 위치에서 오던 기울기가 없다. 09 는 포커스만으로는 버튼이 끌려가지 않고 광택만 중앙에 머문다. 07 은 닫힌 칸을 visibility: hidden 과 tabindex="-1" 로 빼 포커스가 들어가지 못하게 한다.

시연 값을 실제 받기에 그대로 쓰지 않는다

04 의 48MB·2분과 08 의 실패는 시연용으로 정한 값이다. 실제 속도는 들쭉날쭉해서 평균 속도로 구한 남은 시간은 시작 구간에서 크게 틀리고, 08 은 첫 시도 실패와 두 번째 성공을 코드가 정하므로 응답 결과로 바꿔 써야 한다. 03 은 시작·끝 높이를 프레임마다 offsetTop 으로 다시 읽고 가로는 상자 폭의 절반에 고정해 두므로, 버튼이나 트레이가 가운데에서 벗어난 배치에서는 파일이 엉뚱한 자리로 날아간다.

코드 전체와 SCSS 를 직접 만져 보려면 압축 파일을 내려받아 비밀번호 f24sfs72 으로 열면 되고, 아홉 폴더 각각에 download button html css js 로 이루어진 데모 페이지와 스타일이 들어 있다.

변형 세 가지와 접근성

항목 상수 값 느낌
01 SPRING_K 120 → 240 감쇠비가 약 0.41 에서 0.29 로 내려가 오버슛이 눌린 깊이의 24% 에서 39% 로 커지고 출렁임이 더 눈에 띈다
02 CURVE_POW 2 → 4 균일 진행보다 앞서는 폭이 최대 25점에서 약 47점으로 벌어지고 끝 쪽은 더 오래 느리게 간다
05 WAVE_AMP 6 → 12 수위 0.5 에서 수면 진폭이 약 3.9px 에서 7.8px 로 커져 출렁이는 물처럼 보인다

아홉 장면은 모두 진짜 button 이라 Tab 으로 닿고 :focus-visible 윤곽선을 둔다. Enter·Space 는 기본 click 이 받고, 01 만 keydown·keyup 으로 눌림과 놓임을 따로 받는다. 07 은 ArrowDown 으로 열고 화살표로 이동하며 Escape 로 닫는다. prefers-reduced-motion 분기는 아홉 장면 모두 있고, reduce 이면 requestAnimationFrame 루프를 시작하지 않고 입력이 오는 순간 목표 값을 바로 칠한다. 그래서 01 의 출렁임과 09 의 따라가기는 보간 없이 목표 위치로 바로 가고, 03 은 파일 비행 없이 트레이 개수만 늘며 05 는 수위가 곧바로 가득 찬다. 진행 상태는 02·08 이 role="progressbar" 로, 03·04·06·08 이 role="status" 로 알리고, 08 은 색이 아니라 글자와 느낌표 아이콘으로 실패를 전한다.

html css download button 은 라벨만 바꾸면 충분한가?

받는 시간이 짧고 한 번 눌러 끝나는 파일이라면 충분하다. 다만 라벨만 바꾸는 방식은 눌림의 출렁임, 진행의 앞섬, 파일이 간 곳을 따로 보여 주지 못하니, 큰 파일이나 여러 번 받는 화면에서만 위 계산을 얹으면 된다.

받기 말고 다른 버튼 반응이 필요하면 이웃 편을 이어 읽자. 눌림이 돌아오는 움직임은 버튼 눌림 효과 UI 9종 — 눌림 값이 되돌아오는 아홉 가지 방식에, 키프레임 기반 복붙 버튼 모음은 CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + React에 있다.

css download button 전에 자주 묻는 질문

css download button 에서 진행을 전부 JS 로 계산하면 무거운가요?

아홉 장면 모두 전환이나 키프레임 없이 프레임마다 숫자를 계산해 스타일 값 몇 개를 칠하는 방식이다. 입력이 없을 때만 시연이 돌고 reduce 에서는 루프 자체를 시작하지 않으므로, 버튼 하나가 하는 일은 작다.

tailwind css download button 으로도 만들 수 있나요?

이 편의 코드는 일반 CSS 와 JS 이고 Tailwind 를 쓰지 않았다. 움직임이 클래스가 아니라 JS 가 계산한 값에서 나오므로, 마크업의 클래스 이름을 다른 방식으로 바꿔도 계산 구간은 그대로 옮겨 쓸 수 있다.

받기 진행률은 어떻게 실제 값에 연결하나요?

02·04·05·08 의 진행률을 시간 대신 받은 바이트 비율로 바꿔 넣으면 된다. 05 의 수위는 목표가 내려가도 따라 내려가니 입력이 늘어나기만 하는지 확인하고, 04 의 남은 시간 식은 실제 속도가 들쭉날쭉할 때 구간 평균으로 바꾸는 편이 낫다.

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

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