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

투두 리스트 ui 패턴 9종 — 진행률 게이지부터 되감기까지 한 번에 정리

투두 리스트 ui 아홉 패턴 중 하나는 더보기를 누를 때마다 불러오는 개수가 두 배로 늘고, 하나는 되감기를 누르면 완료 타임라인이 방금 순서의 역순으로 사라지며, 하나는 체크박스를 누를 때마다 원형 게이지가 그 비율만큼 즉시 채워집니다.

트리거를 기준으로 늘어놓으면 흐름이 보입니다. 01·03·09 는 click 과 keydown 을 함께 듣고, 02 는 keydown·pointerdown·scroll·wheel 네 가지를 동시에 듣는 유일한 항목이며, 04·08 은 click 하나로 끝나고, 05 는 click·keydown·pointerdown·pointerup·pointerleave 다섯 가지가 겹치고, 06 은 keydown 과 pointerdown·pointermove·pointerup 조합으로 손잡이를 움직이며, 07 은 blur·click·input·keydown 네 가지가 입력창 하나 위에서 겹칩니다. 투두리스트 앱을 실제로 쓸 때 마주치는 손끝 동작(누르기·길게 누르기·끌기·타이핑)을 한 편에 모아 서로 비교하도록 순서를 짰습니다.

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

아홉 개의 해부 — 계산 한 겹이 어디서 도는가

번호 이름 누적면적 강도_평균 움직인프레임
01 할일 하위 작업 펼치기 43.15% 82.7 7/23
02 마감일별 그룹 고정 헤더 8.50% 108.5 8/23
03 리스트→상세 카드 확대 전환 26.05% 149.9 7/23
04 더보기 배치 두 배 로드 6.28% 88.4 6/23
05 길게 눌러 완료 취소 예약 링 18.42% 148.1 16/23
06 재정렬 삽입선 프리뷰 10.54% 176.9 12/23
07 인라인 편집 + 글자 수 카운트다운 7.76% 94.8 11/23
08 완료 타임라인 되감기 9.17% 79.8 22/23
09 체크리스트 진행률 원형 게이지 25.24% 70.4 22/23

화면이 가장 많이 바뀌는 항목은 01 로, 배지가 줄어드는 면적과 하위 목록이 펼쳐지는 면적이 겹쳐 43.15% 까지 올라갑니다. 반대로 04 는 새 항목 몇 개가 아래에 조용히 추가될 뿐이라 6.28% 로 가장 작습니다. 프레임 사이 픽셀 변화의 세기(강도_평균)는 06 이 176.9 로 가장 세고 09 가 70.4 로 가장 약한데, 06 은 삽입선이 얇지만 대비가 강한 점선이라 그렇고 09 는 색 전환이 부드러운 conic-gradient 라 그렇습니다. 24 개 구간 중 실제로 픽셀이 움직인 구간은 08·09 가 22/23 로 가장 촘촘하고 — 하나는 타임라인 점이 하나씩 사라지는 매 프레임이, 하나는 반짝임 스윕이 매 프레임을 채우기 때문입니다 — 04 는 6/23 로 가장 뜸한데 더보기 클릭 사이에는 화면이 그대로 멈춰 있기 때문입니다.

치수를 재서 되먹인다 — 01·03·07

01 은 하위 작업 목록의 scrollHeight 를 재 --h 변수에 넣고, 03 은 카드와 상세 패널 두 요소의 getBoundingClientRect() 좌표 차이를 재며, 07 은 숨은 미러 span 의 실측 너비로 입력창 폭을 정합니다. 셋 다 값을 미리 정해두지 않고 그 순간의 DOM 크기를 읽어 되먹인다는 점이 같은데, 이 좌표계의 정확한 정의는 MDN Element.getBoundingClientRect() 문서에 나와 있습니다.

포인터·스크롤 위치를 판정한다 — 02·06

02 는 scroller.scrollTop 을 각 그룹의 offsetTop 과 비교해 지금 보이는 그룹을 찾고, 06 은 포인터 Y 좌표를 각 행의 경계와 비교해 삽입 지점을 찾습니다. 둘 다 좌표 비교로 "지금 어디인가"만 판정할 뿐, 02 는 그 결과로 헤더 텍스트를 갱신하고 06 은 삽입선 하나만 옮긴다는 차이가 있습니다.

비율을 각도·오프셋으로 바꾼다 — 05·09

05 는 elapsed / LONGPRESS_HOLD_MS 비율을 원 둘레 길이에 곱해 strokeDashoffset 으로, 09 는 done / total 비율에 360 을 곱해 --fill-deg 각도로 바꿉니다. 0~1 사이 값 하나를 선형으로 늘려 그대로 그림으로 옮기는 계산이라 겉보기와 달리 코드는 몇 줄 안 됩니다.

순서·개수를 조작한다 — 04·08

04 는 클릭 횟수의 지수함수로 다음에 불러올 개수를 늘리고, 08 은 total - 1 - i 로 구한 역순 인덱스에 지연 시간을 곱해 사라지는 순서를 뒤집습니다. 둘 다 애니메이션 자체보다 배열 인덱스 연산이 결과 순서를 결정합니다.

패턴별 코드

01할일 하위 작업 펼치기

행을 누르거나 포커스에서 Enter·Space 를 누르면 aria-expanded 값이 먼저 바뀌고, done/total 비율(ratio())로 계산한 만큼 배지가 줄어든 뒤 하위 작업 목록이 scrollHeight 실측값을 --h 로 받아 그 높이까지 펼쳐집니다. 231 의 화살표 회전 펼치기와 달리 트리거는 배지 축소이며, 하위 작업이 딸린 할일을 한 목록에 섞어 쓰는 체크리스트 그룹에 맞습니다.

  function ratio(i) {
    var text = rows[i].badge.textContent; // "1/3" 꼴 — done/total 을 파싱해 축소폭을 계산한다
    var parts = text.split('/');
    var done = parseInt(parts[0], 10);
    var total = parseInt(parts[1], 10);
    return total > 0 ? done / total : 0;
  }

  function measure(sub) {
    // 실제 콘텐츠 높이를 재서 CSS 변수로 넘긴다 — 고정 px 가 아니라 하위 작업 개수만큼 자란다
    sub.style.setProperty('--h', sub.firstElementChild.scrollHeight + 'px');
  }

  function setOpen(i, open) {
    var r = rows[i];
    if (r.open === open) return;
    r.open = open;
    r.btn.setAttribute('aria-expanded', String(open));
    // 배지가 먼저 줄어든다 — 축소폭은 완료 비율로 계산(할 일이 남을수록 더 움츠러들어 눈에 띈다)
    var shrink = 1 - (1 - ratio(i)) * BADGE_SHRINK_MAX;
    r.badge.style.transform = open ? 'scale(' + shrink + ')' : 'scale(1)';
    r.caret.style.transform = open ? 'rotate(180deg)' : 'rotate(0deg)';
    if (open) measure(r.sub);
    r.sub.classList.toggle('open', open);
  }
할일 하위 작업 펼치기
새 창에서 열기

01 · 할일 하위 작업 펼치기 — 측정 누적면적 43.151% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ratio() 배지 축소 계산--h scrollHeight 실측 max-heightaria-expanded

02마감일별 그룹 고정 헤더

keydown·pointerdown·scroll·wheel 을 모두 들어 어떤 조작으로도 반응하고, currentGroupAndRemain() 이 매 프레임 scroller.scrollTop 을 각 그룹의 offsetTop 과 비교해 지금 보이는 그룹과 그 안의 미완료 개수를 다시 셉니다. 231 의 이름 첫 글자 그룹과 달리 마감일(오늘·이번주·나중) 기준이라 캘린더 연동 리스트에 맞습니다.

  // 계산 겹: 스크롤 위치로 "현재 그룹"을 찾고, 그 그룹 안의 미완료 개수를 실시간으로 다시 센다.
  // 미리 계산해 둔 표가 아니라 매 스크롤 프레임마다 DOM 을 다시 읽어 센다.
  function currentGroupAndRemain() {
    var top = scroller.scrollTop;
    var current = groups[0];
    for (var i = 0; i < groups.length; i++) {
      if (groups[i].offsetTop - scroller.offsetTop <= top + 4) current = groups[i];
    }
    var items = current.querySelectorAll('.items li');
    var remain = 0;
    for (var j = 0; j < items.length; j++) {
      if (items[j].getAttribute('data-done') === '0') remain++;
    }
    return { name: current.getAttribute('data-group'), remain: remain };
  }
마감일별 그룹 고정 헤더
새 창에서 열기

02 · 마감일별 그룹 고정 헤더 — 측정 누적면적 8.496% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

currentGroupAndRemain() 스크롤 위치 판정overflow-y:scroll 분리 스크롤러그룹별 미완료 개수 매 프레임 재계산

03리스트→상세 카드 확대 전환

카드를 누르거나 Enter 를 누르면 flipFrom() 이 카드와 상세 패널의 getBoundingClientRect() 좌표 차이를 구해 dx·dy·sx·sy 로 변환하고, transitionend 시점에 다음 상태로 넘어갑니다. 231 의 좌우 슬라이드 전환과 달리 카드가 그 자리에서 화면 크기로 확대되며, 할일 상세 화면 전환에 맞습니다.

  // 계산 겹: 카드의 실제 화면 좌표(First)와 상세 패널이 이미 차지한 화면 전체 좌표(Last)의
  // 차이를 실측해 translate·scale 로 변환한다 — FLIP(First-Last-Invert-Play).
  function flipFrom(cardEl) {
    var c = cardEl.getBoundingClientRect();
    var d = detail.getBoundingClientRect();
    return {
      dx: c.left - d.left,
      dy: c.top - d.top,
      sx: c.width / d.width,
      sy: c.height / d.height
    };
  }
리스트→상세 카드 확대 전환
새 창에서 열기

03 · 리스트→상세 카드 확대 전환 — 측정 누적면적 26.048% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

flipFrom() getBoundingClientRect 좌표차translate()·scale() 좌표 역전overflow:hidden 컨테이너

04더보기 배치 두 배 로드

더보기를 누를 때마다(click) nextBatchSize() 가 BASE_BATCH 에 LOAD_MORE_MULTIPLIER 의 클릭 횟수 제곱을 곱해 다음에 불러올 개수를 지수적으로 늘리고, 늘어난 항목 수는 aria-live 영역으로 안내됩니다. 231 의 고정 세 개씩 순차 슬라이드와 달리 배치 크기 자체가 커지며, 무한 스크롤 대신 페이지네이션을 대체할 때 씁니다.

  var shown = 0;   // 화면에 낸 개수
  var clicks = 0;  // 클릭 횟수 — 계산 겹의 입력값

  // 계산 겹: 다음 묶음 크기는 고정값이 아니라 클릭 횟수의 지수함수다.
  function nextBatchSize() {
    var size = Math.round(BASE_BATCH * Math.pow(LOAD_MORE_MULTIPLIER, clicks));
    var remain = ALL.length - shown;
    return Math.min(size, remain);
  }
더보기 배치 두 배 로드
새 창에서 열기

04 · 더보기 배치 두 배 로드 — 측정 누적면적 6.275% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LOAD_MORE_MULTIPLIER 배치 배율 증가nextBatchSize() 지수 계산aria-live 항목 추가 알림

05길게 눌러 완료 취소 예약 링

누르고 있는 동안(pointerdown) paintRing() 이 elapsed/LONGPRESS_HOLD_MS 비율을 원 둘레 길이에 곱해 strokeDashoffset 을 매 프레임 갱신하고, 링이 다 차기 전에 손을 떼면(pointerup·pointerleave) 예약이 취소되며 Enter·Space 는 이 과정 없이 즉시 확정됩니다. 231 의 즉시 튀어나오는 아이콘 메뉴와 달리 시간차 확인 링이 실수 방지용 완료 취소에 맞습니다.

  // 계산 겹: 실제 경과 시간 비율(elapsed / LONGPRESS_HOLD_MS)을 원의 둘레 길이에 곱해
  // 링이 실제로 몇 초 눌렀는지를 정확히 반영하게 한다.
  function paintRing(i, ratio) {
    var offset = CIRC * (1 - Math.min(1, Math.max(0, ratio)));
    rings[i].style.strokeDashoffset = offset.toFixed(2);
  }

  function startPress(i, startTime) {
    rows[i].classList.add('pressing');
    var raf;
    function tick(now) {
      var elapsed = now - startTime;
      var ratio = elapsed / LONGPRESS_HOLD_MS;
      paintRing(i, ratio);
      if (ratio >= 1) {
        openActions(i);
        cancelPress(i);
        return;
      }
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    rows[i]._raf = raf;
  }
길게 눌러 완료 취소 예약 링
새 창에서 열기

05 · 길게 눌러 완료 취소 예약 링 — 측정 누적면적 18.42% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LONGPRESS_HOLD_MS 경과비율 → strokeDashoffsetpointerdown/pointerup 조기 해제 취소elapsed/LONGPRESS_HOLD_MS 계산

06재정렬 삽입선 프리뷰

손잡이를 누르고 움직이면(pointermove) gapIndexFor() 가 포인터 Y 좌표를 각 행의 실측 경계와 비교해 삽입 지점만 찾고, 실제 항목은 손을 뗄 때(pointerup) 한 번에 옮겨지며 방향키(keydown)로도 같은 판정을 부릅니다. 231 의 파란 빈 줄, 336 의 실시간 드래그 이동과 달리 이동 없이 판정만 하는 가벼운 프리뷰라 저사양 기기용 리스트에 맞습니다.

  // 계산 겹: 포인터 Y 좌표를 각 행의 실측 경계(top/bottom)와 비교해 삽입 지점을 찾는다.
  // 요소를 실시간으로 옮기지 않고, 삽입선 하나만 그 지점으로 옮긴다.
  function gapIndexFor(pointerY, excludeIndex) {
    var rows = Array.prototype.slice.call(list.querySelectorAll('.row'));
    var visible = rows.filter(function (r) { return Number(r.dataset.index) !== excludeIndex; });
    for (var i = 0; i < visible.length; i++) {
      var rect = visible[i].getBoundingClientRect();
      var mid = rect.top + rect.height / 2;
      if (pointerY < mid) return i;
    }
    return visible.length;
  }
재정렬 삽입선 프리뷰
새 창에서 열기

06 · 재정렬 삽입선 프리뷰 — 측정 누적면적 10.539% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

gapIndexFor() 포인터Y 위치 비교점선 삽입선 — 드롭 전 실이동 없음keydown 방향키 재정렬

07인라인 편집 + 글자 수 카운트다운

글자를 클릭하면 blur·input·keydown 을 듣는 진짜 input 요소가 만들어지고, 입력마다 measureWidth() 가 숨은 미러 span 의 getBoundingClientRect().width 를 재 입력창 너비를 맞추며, 남은 글자 수가 MAX_LEN 에 가까워지면 배지가 붉게 흔들립니다. 231 의 연필→체크 아이콘 전환과 달리 글자 수 카운터가 핵심이라 메모 인라인 편집에 맞습니다.

  // 계산 겹: 실제 텍스트 폭을 숨은 미러 span 으로 측정해(scrollWidth) input 너비를 그 값에 맞춘다.
  function measureWidth(text) {
    mirror.textContent = text || ' ';
    var w = mirror.getBoundingClientRect().width + 24;
    return Math.max(MIN_W, Math.min(MAX_W, w));
  }
인라인 편집 + 글자 수 카운트다운
새 창에서 열기

07 · 인라인 편집 + 글자 수 카운트다운 — 측정 누적면적 7.756% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

measureWidth() 미러 span 폭 측정input width 실시간 재계산잔여 글자수 badge shake

08완료 타임라인 되감기

되감기를 누르면(click) rewind() 가 항목마다 total - 1 - i 로 역순 인덱스를 구해 그 값에 STAGGER_MS 를 곱한 만큼 지연을 주고, reduce 상태에서는 이 지연이 곧장 0 이 되며 진행 상황은 aria-live 로 안내됩니다. 231 의 위에서부터 순차 등장과 달리 역순 되감기가 핵심이라 활동 로그를 되짚어볼 때 맞습니다.

  // 계산 겹: 되감기는 최근 항목부터 지우되, 각 항목의 지연 시간을 "끝에서부터의 역순 인덱스"로
  // 계산한다 — 안쪽(오래된 항목)일수록 나중에 사라진다.
  function rewind() {
    var total = shown.length;
    if (total === 0) return;
    var toRemove = shown.slice().reverse();
    toRemove.forEach(function (li, i) {
      var reverseIndex = total - 1 - i; // 뒤에서부터 셀수록 커지는 값
      var delay = reduce ? 0 : reverseIndex * STAGGER_MS * 0.4;
      window.setTimeout(function () {
        li.classList.remove('in');
        li.classList.add('out');
        window.setTimeout(function () {
          if (li.parentNode) li.parentNode.removeChild(li);
          paintLine();
        }, reduce ? 0 : 220);
      }, delay);
    });
    shown = [];
    liveNote.textContent = total + '개 기록을 되감았습니다.';
  }
완료 타임라인 되감기
새 창에서 열기

08 · 완료 타임라인 되감기 — 측정 누적면적 9.168% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAGGER_MS 순차 transitionDelayrewind() 역순 제거 — reverseIndex 지연aria-live 기록/되감기 알림

09체크리스트 진행률 원형 게이지

체크박스(button[role=checkbox])를 누르면 aria-checked 가 바뀌고 paintGauge() 가 done/total 비율을 0~360deg 각도로 바꿔 conic-gradient 로 그리며, 100% 가 되는 순간 한 번만 반짝임이 돕니다. 231 의 취소선 애니메이션과 달리 전체 진행률 게이지가 핵심이라 체크리스트 완료를 축하하는 화면에 맞습니다.

  // 계산 겹: 체크된 개수/전체 개수 비율을 각도(0~360deg)로 바꿔 conic-gradient 로 그린다.
  function paintGauge() {
    var total = rows.length;
    var done = rows.filter(function (r) { return r.dataset.done === '1'; }).length;
    var ratio = done / total;
    var deg = ratio * 360;
    gauge.style.setProperty('--fill-deg', deg + 'deg');
    gaugePct.textContent = Math.round(ratio * 100) + '%';
    if (ratio >= 1 && !wasFull) {
      wasFull = true;
      if (!reduce) {
        shine.classList.remove('play');
        void shine.offsetWidth;
        shine.classList.add('play');
      }
    } else if (ratio < 1) {
      wasFull = false;
    }
  }
체크리스트 진행률 원형 게이지
새 창에서 열기

09 · 체크리스트 진행률 원형 게이지 — 측정 누적면적 25.237% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

paintGauge() 체크비율→각도 conic-gradientGAUGE_RING_WIDTH 링 두께100% shine 스윕 1회(반복 없음)

값이 UI를 바꾸는 곳 — 비율·글자 수·배치 크기·순서

모바일 리스트 ui 처럼 폭이 좁은 화면에서는 상수 하나가 레이아웃 전체를 흔듭니다. 01 은 ratio() 가 돌려주는 배지 축소 비율과 하위 목록의 scrollHeight 실측값이, 02 는 scrollTop 이 가리키는 그룹과 그 안의 미완료 개수가, 03 은 flipFrom() 이 구한 좌표 차이가, 04 는 LOAD_MORE_MULTIPLIER 가, 05 는 elapsed/LONGPRESS_HOLD_MS 비율이, 06 은 gapIndexFor() 가 비교하는 포인터 Y 좌표가, 07 은 MAX_LEN 과 measureWidth() 의 측정값이, 08 은 STAGGER_MS 와 역순 인덱스가, 09 는 GAUGE_RING_WIDTH 와 done/total 비율이 화면에 보이는 크기·색·위치를 그대로 결정합니다.

이웃 가지

손잡이만 있고 요소는 안 움직이는 재정렬은 아코디언 리스트 UI 9가지 — 체크리스트까지의 파란 빈 줄 프리뷰와 이 편의 06 삽입선 프리뷰가 같은 목적을 다른 판정으로 풀고, 항목이 실제로 옮겨 다니는 드래그는 드래그 앤 드롭 UI 리스트 — 목표 인덱스 계산을 얹은 9종의 실시간 이동과 이어지지만 이 편의 06 은 이동 없이 삽입 위치만 미리 보여준다는 점이 갈립니다. 행이 아니라 셀 단위로 값을 고르고 끄는 상호작용은 데이터 테이블 UI 9종 — 열을 끌어 넓히고 셀을 골라 복사하는 표에서 다루고, 이 편은 표가 아니라 리스트 항목 자체의 상태 전환에 집중합니다.

어디서 깨지는가 — 접근성·reduced-motion·키보드

아홉 개를 실제 프로젝트에 그대로 붙여 확인하고 싶다면 다운로드 zip 비밀번호 jq5k5x9y 로 vanilla·react 코드와 prompt.md 를 함께 풀어볼 수 있습니다.

02 의 그룹 헤더는 position:sticky 를 전혀 쓰지 않습니다. .head 는 .scroller 의 형제 요소로 떨어져 있고 .scroller 자신이 overflow-y:scroll 로 스크롤을 갖는 별도 컨테이너라, currentGroupAndRemain() 이 이 scroller.scrollTop 값을 읽어야만 헤더가 갱신됩니다. 이 분리된 스크롤 컨테이너를 없애고 페이지 전체 스크롤로 옮기면 — sticky 처럼 보이는 CSS 만 남고 실제로 읽을 scrollTop 대상이 사라져 — 값이 0 에 멈춰 헤더가 "오늘"에서 그대로 굳습니다.

reduced-motion 아래 남는 것

아홉 항목 모두 .is-demo 가 붙어 있고 prefers-reduced-motion:reduce 가 아닐 때만 자동 재생을 돌리며, 처음 상호작용하는 순간 human() 이 .is-demo 를 떼어 자동 재생을 끕니다. 아홉 개 style.scss 모두 reduced-motion 블록으로 트랜지션 자체를 꺼두는데, 03 은 여기서 더 나아가 transitionend 대신 done() 을 즉시 불러 카드 확대 전환을 건너뛰고, 04 의 등장 지연과 08 의 되감기 지연은 reduce 에서 곧장 0ms 가 되며, 09 는 100% 완료 반짝임 자체를 생략합니다.

키보드·스크린리더로 확인할 것

01 은 aria-expanded 로 펼침 상태를, 09 는 button[role=checkbox]·aria-checked 로 체크 상태를 알리고, 07 은 화면 표시용 span 이 아니라 진짜 input 요소라 스크린리더가 값 입력 자체를 읽습니다. 05 는 Enter·Space 를 누르면 링이 차오르는 과정 없이 openActions() 가 즉시 실행돼 포인터로 길게 누를 때와 결과에 이르는 방식이 다르고, 04·08 은 aria-live="polite" 영역으로 몇 개가 추가되거나 되감겼는지 텍스트로 알립니다.

변형 세 가지와 접근성

이름 값 느낌
체크리스트 게이지 링 두께 키우기 09 GAUGE_RING_WIDTH 6px → 12px GAUGE_RING_WIDTH 를 12px 로 키우면 진행률 변화가 더 도드라져 보인다
롱프레스 확인 링 시간 늘리기 05 LONGPRESS_HOLD_MS 500 → 900 LONGPRESS_HOLD_MS 를 900 으로 늘리면 실수로 완료 취소되는 빈도가 줄어드는 대신 반응이 느리게 느껴진다
더보기 배치 배율 낮추기 04 LOAD_MORE_MULTIPLIER 2.0 → 1.5 LOAD_MORE_MULTIPLIER 를 1.5 로 낮추면 항목이 완만하게 늘어나 스크롤 길이가 급격히 길어지지 않는다

세 변형 모두 값만 바뀔 뿐 aria-expanded·aria-checked·role=checkbox·aria-live 같은 접근성 속성은 그대로 남아, 09 의 게이지가 굵어지거나 05 의 링이 느리게 차거나 04 의 배치가 완만해져도 스크린리더가 읽는 정보는 달라지지 않습니다.

FAQ

투두 리스트 ui 아홉 패턴을 전부 그대로 복사해 써도 되나요?

네, vanilla 폴더의 순수 JS·CSS 로 각 항목이 독립적으로 동작하도록 짰기 때문에 필요한 항목 폴더만 골라 붙여도 됩니다.

체크리스트 ui 진행률은 꼭 원형 게이지여야 하나요?

아닙니다 — paintGauge() 가 계산하는 값은 done/total 비율 하나뿐이라, --fill-deg 를 conic-gradient 대신 막대 너비나 숫자 카운터에 연결해도 같은 데이터로 동작합니다.

리스트→상세 확대 전환은 어떤 화면에 가장 잘 맞나요?

카드 한 장이 그대로 상세 화면이 되는 03 은 목록과 상세를 자주 오가는 좁은 화면에서 가장 자연스럽고, 데스크톱처럼 넓은 화면에서는 오히려 02 의 고정 헤더 쪽이 더 어울립니다.

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

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