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

필터 UI 9종 — 패널 슬라이드부터 칩 제거까지

필터 UI 아홉 개 중 하나는 칩이 사라진 만큼 남은 칩들의 폭 합계를 다시 구해 자리를 좁히고, 하나는 활성 필터 개수 값이 바뀔 때마다 배지 크기를 그 차이만큼 부풀렸다 되돌리며, 또 하나는 두 슬라이더 값의 차이로 채워진 트랙 폭을 다시 계산합니다.

이 아홉 화면은 실제 쇼핑몰 서버나 진짜 상품 데이터가 아니라 가상의 칩·카테고리·가격 값으로 만든 것이며, 01·02·04·06·07·08·09 일곱 개는 클릭·변경·드래그·입력 같은 실제 리스너를 갖고 03·05 둘만 타이머로 자동 재생됩니다.

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

필터 ui 디자인을 찾는 사람은 대부분 패널을 열고 닫는 첫 화면부터 궁금해하지만, 실제로 화면을 흔드는 계산은 아홉 개가 저마다 다른 값 하나를 재서 다른 결과로 바꿉니다. 아래 표는 그 계산이 화면을 실제로 얼마나 흔들었는지 잰 값이고, 순서는 열림·선택 상태(01·07·08) → 개수·순서 값(02·04·06) → 두 값의 차이(03·05·09) 로 계산 성격이 가까운 것끼리 묶었습니다.

실측 한눈에

항목 초점 누적면적 % 강도 평균 움직인 프레임
01 패널 슬라이드 토글 열림 상태 → translateX 보간 41.968 163.3 10/23
02 칩 제거 겹침 남은 칩 폭 → 컨테이너 재계산 5.569 149.5 11/23
03 결과 개수 모프 개수 차이 → rAF 보간 1.142 113.0 18/23
04 전체 해제 캐스케이드 순서 값 → 지연 시점 4.054 74.0 12/23
05 활성 개수 배지 펄스 증감 값 → 스케일 0.34 88.0 14/23
06 카테고리 트리 펼침 자식 개수 → 높이 16.82 140.5 11/23
07 적용 바 스티키 등장 선택 여부 → translateY 19.0 130.7 12/23
08 모바일 시트 드래그 드래그 거리 → 높이 매핑 44.842 30.3 23/23
09 가격 범위 채움 두 값 차이 → 트랙 폭 1.531 160.4 11/23

03·05·09 는 숫자 텍스트·작은 배지·슬라이더 트랙처럼 애초에 몸집이 작은 요소를 움직이는 화면이라 누적면적이 1%대에 머물고, 08 은 시트 전체가 함께 오르내리는 화면이라 44.8%로 가장 큽니다. 강도 평균은 08 이 30.3 으로 가장 낮아 통짜 시트가 부드럽게 움직인다는 뜻이고 01 이 163.3 으로 가장 높아 패널이 열리며 색 차이가 큰 영역이 함께 바뀐다는 뜻입니다. 01·02·06·09 처럼 움직인 프레임이 10~11/23 에 머무는 화면은 값이 바뀌는 시점 사이에 그대로 멈춰 있는 정지 구간이 섞여 있는 것이지, 계산이 도중에 끊긴 결함이 아닙니다.

필터 UI의 해부 — 아홉 화면이 계산하는 것

상태·거리 값을 위치로 바꾸는 축

01·07·08 은 참·거짓이나 이동 거리 같은 값 하나를 화면의 위치로 바꿉니다. 01 은 패널이 열려 있는지(is-open) 값에 따라 transition 이 화면 밖 위치에서 화면 안 위치까지 보간하고, 07 은 체크박스가 하나라도 선택돼 있는지(has-selection) 값에 따라 하단 바를 translateY 로 올리고 내립니다. 08 은 포인터가 이동한 거리(px)를 --drag 값에 그대로 담아 clamp() 로 0~130px 사이만 움직이게 묶습니다.

개수·순서 값을 치수로 바꾸는 축

02·04·06 은 칩이나 하위 항목의 개수·순서를 실제 치수로 바꿉니다. 02 는 recompute() 가 남은 칩마다 getBoundingClientRect().width 를 더하고 간격(GAP_PX)까지 합쳐 컨테이너 폭을 다시 구하며, 06 은 list.scrollHeight 를 재 --tree-height 로 저장해 두었다가 펼쳐질 때 그 값까지 늘어납니다. 04 는 칩의 순서(--i)에 고정 간격(90ms)을 곱해 사라지는 시작 시점을 다시 구해 앞에서부터 차례로 사라지게 합니다.

두 값의 차이를 계산해 다시 그리는 축

03·05·09 는 이전 값과 새 값, 또는 두 입력값의 차이를 계산해 화면을 다시 그립니다. 03 은 이전 결과 수와 새 결과 수 사이를 requestAnimationFrame 으로 나눠 숫자를 하나씩 올리거나 내리고, 05 는 활성 필터 개수가 바뀔 때마다 배지를 scale(1.4) 로 키웠다가 transitionend 를 확인한 뒤에만 1로 되돌립니다. 09 는 최소·최대 두 손잡이 값의 차이를 --min-pct·--max-pct 로 저장해 calc() 가 채워진 구간의 좌우 끝을 다시 그립니다.

화면별 코드

01패널 슬라이드 토글 — 열림 상태 값으로 위치를 보간하는 기본형

쇼핑몰 상품 목록 좌측 필터 패널처럼, 버튼을 누르면 화면 밖에 있던 패널이 안쪽 위치까지 미끄러져 들어옵니다.

열림 여부 하나만 들고 위치를 뒤집는 계산

setOpen(open) 은 stage 요소의 is-open 클래스 하나만 뒤집고, 실제 이동은 CSS transition 이 그 클래스에 걸린 transform 값을 보간해서 처리합니다. 클릭 리스너는 지금 열려 있는지(classList.contains('is-open'))를 뒤집은 값을 그대로 setOpen 에 넘길 뿐입니다.

(function () {
  var stage = document.getElementById('stage');
  var trigger = document.getElementById('trigger');
  var panel = document.getElementById('panel');
  if (!stage || !trigger || !panel) return;

  function setOpen(open) {
    stage.classList.toggle('is-open', open);
    trigger.setAttribute('aria-expanded', String(open));
    panel.setAttribute('aria-hidden', String(!open));
  }

  trigger.addEventListener('click', function () {
    panel.classList.remove('is-demo');
    setOpen(!stage.classList.contains('is-open'));
  });
})();
패널 슬라이드 토글 — 열림 상태 값으로 위치를 보간하는 기본형
새 창에서 열기

01 · 패널 슬라이드 토글 — 열림 상태 값으로 위치를 보간하는 기본형 — 측정 누적면적 41.968% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transformclassList.toggletransition

02칩 제거 겹침 — 남은 칩 개수로 컨테이너 폭을 다시 구하는 줄

적용된 필터를 보여주는 칩 목록 줄에서, 칩 하나를 지우면 남은 칩들이 그 자리를 채우듯 줄 전체가 좁아집니다.

남은 칩 폭을 실측해 컨테이너 폭을 되구하는 계산

recompute() 는 남아 있는 .chip 마다 getBoundingClientRect().width 를 더하고, 첫 번째가 아닌 칩마다 간격(GAP_PX, 8px)을 더해 그 합을 row.style.width 에 그대로 써 넣습니다. 칩을 지우거나(removeChip) 추가할(addChip) 때마다 이 함수를 다시 불러 폭을 그때그때 갱신합니다.

  function recompute() {
    var chips = row.querySelectorAll('.chip');
    var total = 0;
    chips.forEach(function (chip, i) {
      total += chip.getBoundingClientRect().width;
      if (i > 0) total += GAP_PX;
    });
    row.style.width = total + 'px';
  }
칩 제거 겹침 — 남은 칩 개수로 컨테이너 폭을 다시 구하는 줄
새 창에서 열기

02 · 칩 제거 겹침 — 남은 칩 개수로 컨테이너 폭을 다시 구하는 줄 — 측정 누적면적 5.569% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

getBoundingClientRecttransitiongap

03결과 개수 모프 — 이전 값과 새 값 사이를 보간해 세는 숫자

상품 목록 상단의 'N개 결과' 표시에서, 필터 조건이 바뀌어 개수가 달라지면 숫자가 바로 바뀌는 대신 이전 값에서 새 값까지 세듯이 넘어갑니다.

이전 값과 새 값 사이를 rAF 로 보간하는 계산

animate(to, duration) 안의 frame(ts) 는 시작 시각(start)부터 지난 비율(t)을 ease() 곡선으로 굽혀 from 과 to 사이 값을 계산하고, 그 값을 반올림해 el.textContent 에 매 프레임 다시 씁니다. t 가 1에 닿으면 from 을 to 로 갱신하고 다음 값으로 넘어갈 때까지 잠깐 멈춥니다.

  function animate(to, duration) {
    var start = null;
    function frame(ts) {
      if (start === null) start = ts;
      var t = Math.min(1, (ts - start) / duration);
      var value = Math.round(from + (to - from) * ease(t));
      el.textContent = value + '개';
      el.style.setProperty('--pulse', (1 - t).toFixed(3));
      if (t < 1) {
        requestAnimationFrame(frame);
      } else {
        from = to;
        setTimeout(step, HOLD_MS);
      }
    }
    requestAnimationFrame(frame);
  }
결과 개수 모프 — 이전 값과 새 값 사이를 보간해 세는 숫자
새 창에서 열기

03 · 결과 개수 모프 — 이전 값과 새 값 사이를 보간해 세는 숫자 — 측정 누적면적 1.142% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFramesetPropertytextContent

04전체 해제 캐스케이드 — 순서 값에 간격을 곱해 차례로 사라지는 칩

필터 초기화 버튼이 있는 검색 결과 화면에서, 전체 해제를 누르면 칩들이 동시에 사라지지 않고 앞에서부터 차례로 사라집니다.

순서 값에 고정 간격을 곱해 사라지는 시점을 되구하는 계산

클릭 리스너는 각 칩의 순서(i)에 고정 간격(STAGGER_MS, 90ms)을 곱한 값을 setTimeout 지연으로 넘겨, 그 시점에 is-active 클래스를 떼어 냅니다. 클래스가 떨어지는 순간부터는 CSS transition 이 투명도 변화를 보간해 사라지는 움직임을 만듭니다.

  clearBtn.addEventListener('click', function () {
    chips.classList.remove('is-demo');
    var items = chips.querySelectorAll('.chip');
    items.forEach(function (chip, i) {
      setTimeout(function () {
        chip.classList.remove('is-active');
      }, i * STAGGER_MS);
    });
  });
전체 해제 캐스케이드 — 순서 값에 간격을 곱해 차례로 사라지는 칩
새 창에서 열기

04 · 전체 해제 캐스케이드 — 순서 값에 간격을 곱해 차례로 사라지는 칩 — 측정 누적면적 4.054% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animation-delayclassList.removeopacity

05활성 개수 배지 펄스 — 증감 값만큼 부풀렸다 되돌리는 숫자 배지

필터 버튼 옆 활성 개수 배지에서, 적용된 필터 수가 바뀌면 배지가 한 번 부풀었다가 원래 크기로 되돌아와 숫자가 바뀌었다는 걸 알립니다.

값이 바뀐 순간 스케일을 키웠다가 전환이 끝난 뒤에만 되돌리는 계산

grow() 는 다음 값을 배지 문구로 바꾸고 transform 을 scale(1.4) 로 키우기만 하며, 실제로 원래 크기(scale(1))로 되돌리는 시점은 transitionend 리스너가 커진 전환이 끝났는지(e.propertyName === 'transform')를 확인한 뒤에만 실행합니다. 되돌아온 다음에는 HOLD_MS 만큼 쉬었다가 grow() 를 다시 부릅니다.

  function grow() {
    idx = (idx + 1) % VALUES.length;
    badge.textContent = String(VALUES[idx]);
    badge.style.transform = 'scale(1.4)';
  }

  badge.addEventListener('transitionend', function (e) {
    if (e.propertyName !== 'transform') return;
    if (badge.style.transform === 'scale(1.4)') {
      badge.style.transform = 'scale(1)';
    } else {
      setTimeout(grow, HOLD_MS);
    }
  });
활성 개수 배지 펄스 — 증감 값만큼 부풀렸다 되돌리는 숫자 배지
새 창에서 열기

05 · 활성 개수 배지 펄스 — 증감 값만큼 부풀렸다 되돌리는 숫자 배지 — 측정 누적면적 0.34% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transformscale(transitionend

06카테고리 트리 펼침 — 자식 개수로 높이를 다시 구하는 목록

필터 패널 안 카테고리 트리에서, 버튼을 누르면 하위 항목 개수만큼 목록이 정확한 높이까지 늘어나거나 줄어듭니다.

실제 렌더 높이를 재서 펼침 높이로 저장해 두는 계산

measure() 는 목록의 실제 렌더 높이(list.scrollHeight)를 재 --tree-height 커스텀 프로퍼티에 저장해 두고, 클릭 리스너는 is-open 클래스를 뒤집어 CSS 쪽에서 0부터 그 저장된 값까지 높이를 보간하게 합니다. 창 크기가 바뀌어도(resize) 다시 measure() 를 불러 값을 최신으로 유지합니다.

  function measure() {
    list.style.setProperty('--tree-height', list.scrollHeight + 'px');
  }
  measure();

  toggle.addEventListener('click', function () {
    list.classList.remove('is-demo');
    measure();
    var open = list.classList.toggle('is-open');
    toggle.setAttribute('aria-expanded', String(open));
  });
카테고리 트리 펼침 — 자식 개수로 높이를 다시 구하는 목록
새 창에서 열기

06 · 카테고리 트리 펼침 — 자식 개수로 높이를 다시 구하는 목록 — 측정 누적면적 16.82% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scrollHeightsetPropertyaria-expanded

07적용 바 스티키 등장 — 선택 여부 값으로 위치를 다시 잡는 하단 바

모바일 필터 시트의 적용 버튼 바에서, 옵션을 하나라도 고르면 화면 아래 숨어 있던 바가 위로 올라와 자리를 잡습니다.

선택 여부 하나만 보고 위치를 뒤집는 계산

sync() 는 체크박스들 가운데 하나라도 선택돼 있는지를 Array.prototype.some 으로 판단해 has-selection 클래스만 뒤집고, 실제로 바가 올라오고 내려가는 움직임은 그 클래스에 걸린 translateY 값을 CSS transition 이 보간합니다. 체크박스가 바뀔 때(change)마다 이 함수를 다시 부릅니다.

  function sync() {
    bar.classList.remove('is-demo');
    var any = Array.prototype.some.call(inputs, function (input) {
      return input.checked;
    });
    stage.classList.toggle('has-selection', any);
  }

  inputs.forEach(function (input) {
    input.addEventListener('change', sync);
  });
적용 바 스티키 등장 — 선택 여부 값으로 위치를 다시 잡는 하단 바
새 창에서 열기

07 · 적용 바 스티키 등장 — 선택 여부 값으로 위치를 다시 잡는 하단 바 — 측정 누적면적 19.0% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transformtranslateY(classList.toggle

08모바일 시트 드래그 — 이동 거리 값을 높이에 매핑하는 바텀시트

모바일 쇼핑 앱의 필터 바텀시트에서, 손잡이를 잡고 끌면 손가락이 움직인 만큼 시트가 그대로 따라 내려옵니다.

포인터 이동 거리를 그대로 높이 값에 매핑하는 계산

pointerdown 은 시작 좌표(startY)를 잡아 두고 setPointerCapture 로 포인터를 붙잡으며, pointermove 는 그 뒤로 움직인 거리(e.clientY - startY)를 setDrag() 에 그대로 넘겨 --drag 값(0~130px)을 갱신합니다. 손을 떼거나(pointerup) 취소되면(pointercancel) release() 가 값을 0으로 되돌립니다.

  handle.addEventListener('pointerdown', function (e) {
    dragging = true;
    startY = e.clientY;
    sheet.classList.remove('is-demo');
    sheet.classList.add('is-dragging');
    handle.setPointerCapture(e.pointerId);
  });

  handle.addEventListener('pointermove', function (e) {
    if (!dragging) return;
    setDrag(e.clientY - startY);
  });

  function release() {
    if (!dragging) return;
    dragging = false;
    sheet.classList.remove('is-dragging');
    setDrag(0);
  }
  handle.addEventListener('pointerup', release);
  handle.addEventListener('pointercancel', release);
모바일 시트 드래그 — 이동 거리 값을 높이에 매핑하는 바텀시트
새 창에서 열기

08 · 모바일 시트 드래그 — 이동 거리 값을 높이에 매핑하는 바텀시트 — 측정 누적면적 44.842% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointerdownclamp(transform

09가격 범위 채움 — 두 입력 값의 차이로 트랙 폭을 구하는 슬라이더

쇼핑몰 가격 범위 필터에서, 최소·최대 손잡이를 움직이면 그 사이가 채워진 막대로 다시 그려집니다.

두 입력 값을 비교해 채움 구간의 좌우 끝을 되구하는 계산

update() 는 최소·최대 입력값을 숫자로 읽어(값이 뒤집혔으면 서로 바꾸고) --min-pct·--max-pct 두 커스텀 프로퍼티에 저장하며, CSS calc() 가 이 두 값의 차이로 채움 막대의 시작·끝 위치를 그립니다. 두 input[type=range] 모두 input 이벤트마다 이 함수를 다시 부릅니다.

  function update() {
    var minV = Number(minInput.value);
    var maxV = Number(maxInput.value);
    if (minV > maxV) { var t = minV; minV = maxV; maxV = t; }
    fill.style.setProperty('--min-pct', minV);
    fill.style.setProperty('--max-pct', maxV);
    if (minLabel) minLabel.textContent = priceLabel(minV);
    if (maxLabel) maxLabel.textContent = priceLabel(maxV);
  }
가격 범위 채움 — 두 입력 값의 차이로 트랙 폭을 구하는 슬라이더
새 창에서 열기

09 · 가격 범위 채움 — 두 입력 값의 차이로 트랙 폭을 구하는 슬라이더 — 측정 누적면적 1.531% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

input[type=range]calc(setProperty

값이 화면을 바꾸는 곳 — 남은 폭·활성 개수·범위 차이

모바일 필터 ui 시트처럼 손가락으로 직접 끄는 화면도, 결국은 값 하나가 바뀔 때마다 치수를 다시 재는 같은 원리로 움직입니다. 아래 세 갈래는 그 값이 어떤 치수로 바뀌는지를 기준으로 앞서 다룬 아홉 화면을 다시 묶은 것입니다.

남은 폭 — 사라진 만큼 컨테이너를 다시 재는 곳

02 는 칩 하나가 사라질 때마다 남은 칩들의 실측 폭과 간격을 다시 더해 컨테이너 폭을 구합니다. 06 도 같은 성격으로, 자식 항목 개수가 아니라 그 항목들이 실제로 차지하는 렌더 높이(scrollHeight)를 재서 펼침 높이를 정합니다. 둘 다 값을 지어내지 않고 getBoundingClientRect·scrollHeight 로 화면에서 직접 잰다는 공통점이 있습니다.

활성 개수 — 값의 증감을 스케일·지연 시점으로 바꾸는 곳

05 는 활성 필터 개수가 바뀐 상태가 되면 배지를 한 번 부풀렸다 되돌려 변화를 알리고, 04 는 칩의 순서 값에 고정 간격을 곱해 사라지는 시작 시점을 다시 구합니다. 03 도 개수 값 자체가 바뀌는 화면인데, 여기서는 배지나 지연이 아니라 숫자 텍스트를 이전 값에서 새 값까지 세는 방식으로 바뀝니다.

범위 차이 — 두 값 사이 거리를 좌표로 바꾸는 곳

09 는 최소·최대 두 손잡이 값의 차이를 트랙 전체 길이에 대한 비율로 계산해 채움 구간의 시작과 끝을 다시 그립니다. 08 도 넓게 보면 비슷한 계산인데, 두 입력값의 차이 대신 드래그 시작점과 지금 위치의 차이(px)를 시트 높이 값에 그대로 매핑합니다.

어디서 깨지는가 — 빠른 연속 해제·모바일 드래그·낮은 대비

빠른 연속 해제 — 캐스케이드 도중에 다시 누르면 겹친다

04 의 클릭 리스너는 전체 해제 버튼을 다시 눌러도 스스로를 잠그지 않습니다. 캐스케이드가 끝나기 전에 버튼을 한 번 더 누르면 각 칩마다 setTimeout 이 새로 걸려 이미 걸려 있던 타이머와 겹치는데, classList.remove('is-active') 자체는 몇 번을 불러도 결과가 같아 눈에 띄는 오류로는 안 보이지만 실제 서비스에서 초기화 뒤 곧바로 새 필터를 추가하는 코드를 붙인다면 mbfmp3s6 안 원본에서 이 지점에 버튼 비활성화나 진행 중 플래그를 먼저 넣어야 안전합니다.

모바일 드래그 — 이동 범위가 시트 높이와 무관하게 고정돼 있다

08 의 MAX_DRAG 는 130px 로 고정된 값이라, 실제 서비스에서 시트 안 내용이 이보다 훨씬 길면 다 내려도 시트가 완전히 닫히지 않은 것처럼 보일 수 있습니다. pointerdown·pointermove·pointerup·pointercancel 네 이벤트를 그대로 옮겨 쓰더라도, MAX_DRAG 값만은 실제 시트의 렌더 높이에 맞춰 다시 재야 합니다.

낮은 대비 — 작은 배지·라벨일수록 색 차이가 더 중요해진다

05 의 숫자 배지나 09 의 가격 라벨처럼 글자가 작은 요소는 배경과 글자 색의 밝기 차이가 조금만 줄어도 값이 바뀐 것을 놓치기 쉽습니다. 필터 팔레트를 파스텔 톤으로 고를수록 이 차이가 더 잘 드러나므로, 실제로 붙일 때는 배지·라벨 색 조합을 WCAG 기준으로 직접 재보는 편이 안전합니다.

변형 세 가지와 접근성

변형 바꾸는 값 바뀌는 느낌
칩 폭 재계산 트랜지션 시간 02 칩 컨테이너 width 트랜지션 지금 300ms → 150ms·400ms 로 조정 150ms 로 두면 칩이 사라지자마자 줄이 즉시 좁혀져 딱딱하게 느껴지고, 400ms 로 늘리면 남은 칩들이 부드럽게 미끄러져 붙지만 빠르게 여러 개를 지울 때는 줄이 계속 흔들리는 인상을 준다
클리어 캐스케이드 인덱스 간격 04 STAGGER_MS 지금 90ms → 40ms·140ms 로 조정 40ms 로 두면 칩 여러 개가 거의 동시에 사라져 캐스케이드가 잘 안 보이고, 140ms 로 늘리면 칩이 많을수록 전체 초기화가 오래 걸려 답답하게 느껴질 수 있다
트리 펼침 높이 트랜지션 곡선 06 height 트랜지션 지금 cubic-bezier(0.2, 0.8, 0.2, 1) → 오버슈트가 있는 cubic-bezier(0.34, 1.56, 0.64, 1) 로 조정 지금 곡선은 목록이 매끄럽게 늘어나 차분한 인상을 주고, 오버슈트가 있는 커브는 다 펼쳐진 뒤 살짝 더 늘어났다 줄어들어 하위 항목이 많다는 것을 몸으로 느끼게 한다

접근성

아홉 화면 모두 prefers-reduced-motion 값을 확인해, 그 값이 켜진 상태가 되면 트랜지션을 끄고 정지된 값 하나만 보여줍니다. 01 은 aria-expanded·aria-hidden 을, 06 도 같은 aria-expanded 를 버튼과 목록에 두어 펼침 여부를 스크린 리더가 읽을 수 있게 했고, 08 은 손잡이 버튼에 aria-label 로 "시트 드래그해서 닫기" 라는 대체 설명을 달아 두어 포인터 조작이 어려운 사람도 버튼임을 알 수 있습니다. 07 은 체크박스 자체가 이미 키보드로 접근 가능해 별도 처리를 더하지 않았습니다.

FAQ

이 아홉 화면은 실제 쇼핑몰의 필터 ui 디자인을 그대로 옮긴 건가요?

아니요. 실제 쇼핑몰 서버나 진짜 상품·브랜드 데이터는 어디에도 없고, 색상·사이즈·가격 같은 값은 전부 이 데모를 위해 만든 가상의 값입니다. 계산 방식(값 하나를 재서 치수로 바꾸는 원리)은 그대로 옮겨 쓸 수 있지만, 화면에 보이는 상품명·가격은 재현이 아니라 예시입니다.

아홉 화면 중 실제로 손으로 조작할 수 있는 건 몇 개인가요?

01·02·04·06·07·08·09 일곱 개는 클릭·체크박스 변경·드래그·슬라이더 입력 같은 실제 리스너가 붙어 있어 손으로 눌러 볼 수 있습니다. 03(결과 개수 모프)과 05(활성 개수 배지 펄스)는 리스너 없이 타이머로 값을 순환시키는 자동 시연이라, 실제 서비스에 붙일 때는 필터 결과·개수가 바뀌는 지점에 이 계산을 직접 연결해야 합니다.

다중 선택 필터 ui 에서 칩을 여러 개 한꺼번에 지워도 폭 계산이 깨지지 않나요?

이 데모의 02 는 칩 하나를 지울 때마다 recompute() 를 한 번씩 부르는 구조라 여러 칩을 순서대로 지우면 그때마다 폭이 다시 맞춰집니다. 다만 여러 칩을 한 번에 지우는 다중 선택 UI 자체는 이 아홉 화면에 없으므로, 그런 화면을 만든다면 제거가 끝난 뒤 한 번만 recompute() 를 부르도록 직접 손봐야 합니다.

더 읽을거리

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

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