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

범위 슬라이더 9종 — 핸들 두 개가 서로 못 넘어가는 계산까지

범위 슬라이더 9종 가운데 하나는 두 핸들이 최소 간격 아래로는 절대 겹치지 않게 계산으로 막고, 하나는 두 값이 가까워지면 풍선 라벨 두 개가 하나로 합쳐지며, 하나는 트랙 뒤에 막대 그래프가 깔려 값 분포를 함께 보여줍니다.

값 하나만 정하는 input[type=range] 대신 최소·최대 두 값을 동시에 정하는 이중 핸들 UI를 모은 갤러리로, 01·03·04·06·08·09 는 keydown 과 pointerdown/pointermove/pointerup 리스너 하나로 핸들을 움직이고 02·07 은 여기에 resize 리스너를, 05 는 focusin 리스너를 더 얹습니다. 아홉 항목 모두 Math.random 없이 결정적으로 계산되고, 08 하나만 requestAnimationFrame 으로 스프링 물리를 계산하며 나머지 여덟 개는 rAF 없이 포인터 이벤트가 값을 바로 갱신하고 CSS transition 이 이동을 그립니다.

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

아래 순서는 값 충돌을 막는 계산(01)에서 시작해 시각 보조(02·03·06)를 거쳐 조작 방식 확장(04·05·07)과 물리 계산(08), 접근성 비교(09)로 이어집니다.

아홉 가지 이중 핸들 슬라이더 — 계산 한 겹이 어디서 도는가

항목 누적면적 강도_평균 움직인프레임
01 두 핸들이 서로 못 넘어가게 막는 범위 슬라이더 3.06% 176.2 22/23
02 두 핸들 사이만 색이 채워지는 범위 슬라이더 3.80% 101.0 21/23
03 값이 가까워지면 풍선 두 개가 하나로 합쳐지는 범위 슬라이더 8.16% 111.0 21/23
04 가운데를 끌면 두 핸들이 같이 움직이는 범위 슬라이더 4.11% 55.4 17/23
05 Tab 으로 두 핸들을 번갈아 조정하는 범위 슬라이더 3.22% 145.1 21/23
06 히스토그램 배경 위에서 구간을 고르는 범위 슬라이더 6.34% 87.0 21/23
07 10 단위로 눈금에 붙는 범위 슬라이더 5.15% 129.7 21/23
08 최소 간격을 지키려고 탄성으로 밀리는 범위 슬라이더 3.03% 159.9 21/23
09 모션 줄임과 일반 모션을 나란히 비교하는 범위 슬라이더 5.48% 129.3 21/23

누적면적이 가장 큰 항목은 03 으로 8.16%이고, 두 번째는 06 의 6.34%입니다 — 03 은 풍선 두 개가 겹치기 직전 하나로 합쳐지며 넓게 다시 그려지고, 06 은 막대 24개 가운데 강조 구간이 통째로 켜졌다 꺼지기 때문입니다. 가장 작은 항목은 08 로 3.03%인데 01(3.06%)과 거의 같은 수준으로, 두 항목 다 핸들 두 개만 좁게 오갑니다. 강도_평균이 가장 높은 항목은 01 하나로 176.2까지 오르는데, 핸들이 GAP 경계에 닿아 멈추는 순간 프레임 사이 픽셀 차가 크게 튑니다. 가장 낮은 항목은 04 로 55.4인데, 구간 전체가 같은 폭으로 완만하게 평행 이동만 하기 때문입니다. 23개 델타 구간 가운데 움직인프레임이 가장 많은 항목은 01 로 22/23 이고, 가장 적은 항목은 04 로 17/23 입니다.

최소값이 최대값보다 못 넘어가게 막는다 — 01

GAP(14)을 넘지 못하도록 setLo·setHi 가 서로의 값을 기준으로 매 입력마다 다시 clamp 합니다.

실측 트랙 폭을 px 로 환산해 채움 폭을 읽어준다 — 02

getBoundingClientRect 로 잰 트랙 폭에 (hi-lo)% 를 곱해 pxOut 에 그대로 씁니다.

풍선 사이 화면 거리가 MERGE_PX 보다 좁아지면 합친다 — 03

트랙 실측 폭으로 구한 두 값의 px 간격이 MERGE_PX(46) 미만이면 is-merged 클래스를 켭니다.

폭을 유지한 채 구간 전체를 평행 이동시킨다 — 04

shiftBoth() 가 hi-lo 폭은 그대로 두고 두 값에 같은 delta 를 더한 뒤 트랙 끝단에서 clamp 합니다.

focusin 으로 지금 초점이 있는 핸들만 추적한다 — 05

Tab 으로 핸들에 초점이 옮겨질 때마다 setActive 가 활성 핸들을 기록해 방향키 반응 대상을 좁힙니다.

Math.random 없이 고정 배열로 분포를 그린다 — 06

HEIGHTS 24개 값을 순서대로 막대로 그리고, 각 막대 중심이 [lo, hi] 안에 들어오는지로 강조 여부를 정합니다.

눈금을 실측 픽셀 폭으로 다시 배치한다 — 07

STEP(10) 단위로 반올림해 값을 스냅하고, layoutTicks 가 트랙 실측 폭으로 눈금 위치를 px 로 다시 계산합니다.

반대쪽을 밀어내고 스프링으로 뒤쫓는다 — 08

간격이 MIN_GAP(12) 아래로 좁혀지면 반대쪽 목표값을 함께 밀어내고, 화면 위치는 K(210)·DAMP(18) 스프링 물리로 목표를 뒤쫓아 오버슈트합니다.

같은 상태를 트랜지션 유무만 다르게 그린다 — 09

왼쪽 패널은 CSS transition 으로 이동하고, 오른쪽 미리보기는 애초에 transition 규칙이 없어 같은 값을 항상 즉시 이동으로 보여줍니다.

패턴별 코드

아홉 항목의 계산부 코드만 원본 그대로 발췌했고, 줄 순서와 들여쓰기도 손대지 않았습니다.

01두 핸들이 서로 못 넘어가게 막는 범위 슬라이더

최소값 핸들을 최대값 핸들 쪽으로 밀어도 정해둔 최소 간격(14)보다 가까워지지 않고 그 지점에서 더는 움직이지 않습니다. 최소·최대 가격을 동시에 정하는 필터에 맞습니다.

  function paint() {
    lo.style.left = v.lo + '%'; hi.style.left = v.hi + '%';
    fill.style.left = v.lo + '%'; fill.style.width = (v.hi - v.lo) + '%';
    lo.setAttribute('aria-valuenow', String(v.lo)); hi.setAttribute('aria-valuenow', String(v.hi));
    lo.setAttribute('aria-valuetext', String(v.lo)); hi.setAttribute('aria-valuetext', String(v.hi));
    loOut.textContent = String(v.lo); hiOut.textContent = String(v.hi);
  }
  // 계산 겹: 최소값이 최대값 - GAP 을 넘지 못하게, 최대값이 최소값 + GAP 아래로 못 내려가게 막는다
  function setLo(n) {
    var blocked = document.getElementById('track');
    v.lo = Math.max(MIN, Math.min(n, v.hi - GAP));
    lo.classList.toggle('is-blocked', n > v.hi - GAP);
    paint();
  }
  function setHi(n) {
    v.hi = Math.min(MAX, Math.max(n, v.lo + GAP));
    hi.classList.toggle('is-blocked', n < v.lo + GAP);
    paint();
  }
두 핸들이 서로 못 넘어가게 막는 범위 슬라이더
새 창에서 열기

01 · 두 핸들이 서로 못 넘어가게 막는 범위 슬라이더 — 측정 누적면적 3.058% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GAP=14role=slideraria-valuetext

02두 핸들 사이만 색이 채워지는 범위 슬라이더

트랙 전체가 아니라 낮은 핸들과 높은 핸들 사이 구간만 색이 채워지고, 핸들을 움직이면 채워진 구간의 양 끝이 그 위치를 따라갑니다 — 224(css-picker-ui-patterns)의 듀얼 섬 가격 범위 슬라이더는 값 갱신과 단순 채움만 보여주지만, 이 항목은 getBoundingClientRect 로 실측한 트랙 폭으로 채움 폭을 px 로 환산해 읽어주고 자동 재생 스윕 위상까지 더합니다. 선택한 구간이 한눈에 보여야 하는 통계 필터에 맞습니다.

  // 계산 겹: 두 값을 트랙 채움 %로 바꾸고, 실측 트랙 폭(getBoundingClientRect)으로 채움을 px 로도 환산한다.
  function paint() {
    lo.style.left = v.lo + '%'; hi.style.left = v.hi + '%';
    fill.style.left = v.lo + '%'; fill.style.width = (v.hi - v.lo) + '%';
    var sweepPct = v.lo + (v.hi - v.lo) * v.phase;
    var trackPct = v.hi - v.lo === 0 ? 0 : (sweepPct - v.lo) / (v.hi - v.lo) * 100;
    sweep.style.left = trackPct + '%';
    lo.setAttribute('aria-valuenow', String(v.lo)); hi.setAttribute('aria-valuenow', String(v.hi));
    lo.setAttribute('aria-valuetext', String(v.lo)); hi.setAttribute('aria-valuetext', String(v.hi));
    loOut.textContent = String(v.lo); hiOut.textContent = String(v.hi);
    var r = track.getBoundingClientRect();
    pxOut.textContent = String(Math.round(r.width * (v.hi - v.lo) / 100));
  }
  function setLo(n) { v.lo = Math.max(MIN, Math.min(n, v.hi)); paint(); }
  function setHi(n) { v.hi = Math.min(MAX, Math.max(n, v.lo)); paint(); }
두 핸들 사이만 색이 채워지는 범위 슬라이더
새 창에서 열기

02 · 두 핸들 사이만 색이 채워지는 범위 슬라이더 — 측정 누적면적 3.797% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

getBoundingClientRectpxOutaria-valuetext

03값이 가까워지면 풍선 두 개가 하나로 합쳐지는 범위 슬라이더

핸들마다 값 풍선이 따라다니다가 두 값이 서로 가까워지면 풍선 두 개가 겹치지 않도록 하나의 풍선에 두 값을 나란히 적습니다. 좁은 구간을 정밀하게 고르는 예약 날짜 필터에 맞습니다.

  // 계산 겹: 두 값 풍선의 실제 화면 간격(px)을 트랙 실측 폭으로 구해 MERGE_PX 보다 좁으면 하나로 합친다.
  function paint() {
    lo.style.left = v.lo + '%'; hi.style.left = v.hi + '%';
    fill.style.left = v.lo + '%'; fill.style.width = (v.hi - v.lo) + '%';
    bLo.style.left = v.lo + '%'; bHi.style.left = v.hi + '%';
    bMerged.style.left = (v.lo + v.hi) / 2 + '%';
    lo.setAttribute('aria-valuenow', String(v.lo)); hi.setAttribute('aria-valuenow', String(v.hi));
    lo.setAttribute('aria-valuetext', String(v.lo)); hi.setAttribute('aria-valuetext', String(v.hi));
    bLo.textContent = String(v.lo); bHi.textContent = String(v.hi);
    bMerged.textContent = v.lo + ' – ' + v.hi;
    var r = track.getBoundingClientRect();
    var gapPx = r.width * (v.hi - v.lo) / 100;
    var merged = gapPx < MERGE_PX;
    root.classList.toggle('is-merged', merged);
  }
값이 가까워지면 풍선 두 개가 하나로 합쳐지는 범위 슬라이더
새 창에서 열기

03 · 값이 가까워지면 풍선 두 개가 하나로 합쳐지는 범위 슬라이더 — 측정 누적면적 8.158% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MERGE_PX=46getBoundingClientRectrole=slider

04가운데를 끌면 두 핸들이 같이 움직이는 범위 슬라이더

채워진 구간 가운데를 잡고 끌면 두 핸들 사이 간격은 그대로 둔 채 구간 전체가 함께 밀리고, 끝에 닿으면 그 자리에서 멈춥니다. 고른 기간을 폭은 유지한 채 앞뒤로 옮기는 일정 필터에 맞습니다.

  // 계산 겹: 폭(hi-lo)은 그대로 두고 두 값을 같은 delta 만큼 평행 이동시키되, 끝단에 닿으면 그 자리에서 멈춘다.
  function shiftBoth(deltaPct) {
    var w = v.hi - v.lo;
    var nlo = v.lo + deltaPct;
    nlo = Math.max(MIN, Math.min(nlo, MAX - w));
    v.lo = nlo; v.hi = nlo + w;
    paint();
  }
가운데를 끌면 두 핸들이 같이 움직이는 범위 슬라이더
새 창에서 열기

04 · 가운데를 끌면 두 핸들이 같이 움직이는 범위 슬라이더 — 측정 누적면적 4.107% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

shiftBoth()getBoundingClientRectrole=slider

05Tab 으로 두 핸들을 번갈아 조정하는 범위 슬라이더

Tab 키로 낮은 핸들과 높은 핸들 사이에 초점이 번갈아 옮겨가고, 방향키를 누르면 지금 초점이 있는 핸들만 한 단계씩 움직이며 최소 간격은 그대로 지켜집니다. 마우스 없이 값을 정확히 맞추는 폼 필터에 맞습니다.

  // 계산 겹: focusin/focusout 으로 지금 초점이 어느 핸들인지 추적해 그 핸들만 방향키로 움직인다.
  function setActive(which) {
    root.classList.toggle('is-active-lo', which === 'lo');
    root.classList.toggle('is-active-hi', which === 'hi');
    status.textContent = which === 'lo' ? 'Tab · 최소값 초점' : 'Tab · 최대값 초점';
  }
  lo.addEventListener('focusin', function () { setActive('lo'); });
  hi.addEventListener('focusin', function () { setActive('hi'); });
Tab 으로 두 핸들을 번갈아 조정하는 범위 슬라이더
새 창에서 열기

05 · Tab 으로 두 핸들을 번갈아 조정하는 범위 슬라이더 — 측정 누적면적 3.223% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

focusinaria-live=politerole=slider

06히스토그램 배경 위에서 구간을 고르는 범위 슬라이더

트랙 바로 뒤에 값 구간별 막대 그래프가 깔려 있어, 핸들로 고른 구간이 전체 분포 중 어디에 해당하는지 막대 높이로 함께 보입니다. 가격대별 매물 수 분포를 같이 보여주는 검색 필터에 맞습니다.

  // 결정론적 분포 — Math.random 없이 고정 배열로 막대 높이를 정한다 (%, 0~100)
  var HEIGHTS = [22, 40, 65, 38, 52, 70, 45, 60, 33, 48, 80, 55, 42, 66, 30, 58, 44, 72, 50, 36, 62, 48, 54, 40];
  var root = document.getElementById('ds');
  var hist = document.getElementById('hist');
  var track = document.getElementById('track');
  var fill = document.getElementById('fill');
  var lo = document.getElementById('thumbLo');
  var hi = document.getElementById('thumbHi');
  var loOut = document.getElementById('loOut');
  var hiOut = document.getElementById('hiOut');
  var v = { lo: 30, hi: 72 };
  var bars = [];

  HEIGHTS.forEach(function (h, i) {
    var bar = document.createElement('div');
    bar.className = 'ds__bar';
    bar.style.left = (i / HEIGHTS.length * 100) + '%';
    bar.style.width = (100 / HEIGHTS.length * 0.7) + '%';
    bar.style.height = h + '%';
    hist.appendChild(bar);
    bars.push({ el: bar, center: (i + 0.5) / HEIGHTS.length * 100 });
  });
히스토그램 배경 위에서 구간을 고르는 범위 슬라이더
새 창에서 열기

06 · 히스토그램 배경 위에서 구간을 고르는 범위 슬라이더 — 측정 누적면적 6.338% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HEIGHTS.forEach()role=slideraria-valuenow

0710 단위로 눈금에 붙는 범위 슬라이더

핸들을 움직이는 즉시 10 단위 눈금으로 반올림되어 멈추고, 낮은 핸들과 높은 핸들의 값은 서로를 넘어서지 않도록 항상 비교해 고정됩니다. 정해진 단위로만 값을 고르는 수량 필터에 맞습니다.

  // 계산 겹: 눈금을 %가 아니라 트랙의 실측 픽셀 폭(getBoundingClientRect)으로 위치를 다시 계산해 배치한다.
  function layoutTicks() {
    var r = track.getBoundingClientRect();
    ticks.forEach(function (t) {
      var x = r.width * (t.value - MIN) / (MAX - MIN);
      t.el.style.left = Math.round(x) + 'px';
    });
  }
  function snap(n) { return Math.max(MIN, Math.min(MAX, Math.round(n / STEP) * STEP)); }
10 단위로 눈금에 붙는 범위 슬라이더
새 창에서 열기

07 · 10 단위로 눈금에 붙는 범위 슬라이더 — 측정 누적면적 5.147% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STEP=10getBoundingClientRectrole=slider

08최소 간격을 지키려고 탄성으로 밀리는 범위 슬라이더

두 핸들 간격이 최소값 아래로 좁혀지려 하면 반대쪽 핸들이 함께 밀려나 최소 간격을 지키고, 화면 위치는 질량-감쇠 스프링 물리로 목표값을 뒤쫓아 살짝 오버슈트하며 따라갑니다. 간격이 너무 좁아지면 안 되는 인원수 범위 필터에 맞습니다.

  var last = null;
  function springFrame(ts) {
    if (reduce) { pos.lo.v = target.lo; pos.hi.v = target.hi; paintVisual(); return; }
    if (last === null) last = ts;
    var dt = Math.min(0.032, (ts - last) / 1000); last = ts;
    ['lo', 'hi'].forEach(function (k) {
      var s = pos[k], t = target[k];
      var force = -K * (s.v - t) - DAMP * s.vel;
      s.vel += force * dt; s.v += s.vel * dt;
    });
    paintVisual();
    requestAnimationFrame(springFrame);
  }
  if (!reduce) requestAnimationFrame(springFrame);
최소 간격을 지키려고 탄성으로 밀리는 범위 슬라이더
새 창에서 열기

08 · 최소 간격을 지키려고 탄성으로 밀리는 범위 슬라이더 — 측정 누적면적 3.027% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MIN_GAP=12requestAnimationFramerole=slider

09모션 줄임과 일반 모션을 나란히 비교하는 범위 슬라이더

같은 두 핸들 값을 패널 두 개에 동시에 그리되, 왼쪽은 위치 변화에 트랜지션이 걸리고 오른쪽 미리보기는 트랜지션 없이 즉시 이동해 축소 모션 결과를 나란히 비교해 보여줍니다. 동작 민감 사용자를 위한 접근성 대체 모드에 맞습니다.

  // 계산 겹: 같은 상태값 하나를 두 패널에 동시에 그리되, 오른쪽 미리보기는 transition 을 원천적으로 갖지 않아
  // "모션 줄임" 사용자가 보는 즉시-이동 결과를 항상 함께 보여준다 (실제 OS 설정과 무관한 비교용).
  function paint() {
    lo.style.left = v.lo + '%'; hi.style.left = v.hi + '%';
    fillA.style.left = v.lo + '%'; fillA.style.width = (v.hi - v.lo) + '%';
    loB.style.left = v.lo + '%'; hiB.style.left = v.hi + '%';
    fillB.style.left = v.lo + '%'; fillB.style.width = (v.hi - v.lo) + '%';
    lo.setAttribute('aria-valuenow', String(v.lo)); hi.setAttribute('aria-valuenow', String(v.hi));
    lo.setAttribute('aria-valuetext', String(v.lo)); hi.setAttribute('aria-valuetext', String(v.hi));
    loOutA.textContent = String(v.lo); hiOutA.textContent = String(v.hi);
    loOutB.textContent = String(v.lo); hiOutB.textContent = String(v.hi);
  }
모션 줄임과 일반 모션을 나란히 비교하는 범위 슬라이더
새 창에서 열기

09 · 모션 줄임과 일반 모션을 나란히 비교하는 범위 슬라이더 — 측정 누적면적 5.484% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-hidden=trueprefers-reduced-motionrole=slider

값이 UI를 바꾸는 곳 — 최소 간격·스냅 눈금 간격·툴팁 합체 거리

01 은 GAP(14)이 두 핸들이 가까워질 수 있는 한계를 정합니다 — 이 값을 낮추면 가격 슬라이더처럼 좁은 범위를 지정하는 UI에서 최소·최대를 더 붙여 고를 수 있게 됩니다. 03 은 MERGE_PX(46)가 두 값 풍선이 하나로 합쳐지는 화면 거리 기준이고, 07 은 STEP(10)이 핸들이 멈출 수 있는 눈금 간격을, 08 은 MIN_GAP(12)·K(210)·DAMP(18) 세 상수가 각각 하드 클램프 거리·스프링 강도·감쇠율을 정해 오버슈트 크기와 잦아드는 속도를 함께 결정합니다.

이웃 가지

같은 입력 축에서도 초점은 갈립니다. 핸들 하나로 값 하나만 정하는 css input range 슬라이더 — 관성·스냅 계산을 얹은 커스텀 9종 과 달리, 이 편은 처음부터 최소·최대 두 값을 함께 다루는 이중 핸들에 집중합니다. 날짜·가격 범위를 고르는 UI를 캘린더 형태로 더 보려면 달력 선택 UI 9종 — 복붙하면 되는 을, 값이 아니라 목록 자체를 좁히는 흐름을 보려면 필터 UI 9종 — 패널 슬라이드부터 칩 제거까지 를 함께 열어보면 됩니다.

어디서 깨지는가 — 접근성·reduced-motion·핸들 겹침 처리

실제로 겪은 함정은 상수마다 단위 도메인이 다르다는 점입니다. 01 의 GAP·07 의 STEP·08 의 MIN_GAP 은 전부 v.lo·v.hi 와 같은 값 도메인(0~100)에서 비교되지만, 03 의 MERGE_PX 는 track.getBoundingClientRect().width 로 잰 화면 픽셀 거리와 비교됩니다. MIN·MAX 를 0~100 이 아니라 실제 가격 범위(예: 0~500)로 바꾸면 GAP·STEP·MIN_GAP 은 새 값 범위에 맞게 다시 정해야 하지만, MERGE_PX 는 화면 폭 기준이라 그대로 둬도 됩니다 — 넷을 같은 성격의 숫자로 보고 한꺼번에 스케일하면 03 만 계산이 어긋납니다.

reduce 상태에서 각자 무엇을 멈추는가

아홉 항목 모두 로드 시점에 window.matchMedia('(prefers-reduced-motion: reduce)').matches 값을 한 번 읽어 reduce 가 참이면 자동 시연 timer 를 아예 만들지 않습니다(9/9). 01~07·09 는 여기에 더해 scss .ds.is-demo .ds__thumb·.ds__fill 의 transition 규칙 자체를 reduce 미디어 쿼리 안에서 지워, 자동 루프가 혹시 남아 있어도 트랜지션 없이 즉시 이동만 하게 만듭니다. 08 은 애초에 CSS transition을 쓰지 않고 위치를 rAF 스프링으로만 계산하므로, reduce 가 참이면 springFrame 이 스프링 계산을 건너뛰고 목표값으로 즉시 이동합니다.

위 격자에 있는 아홉 개 슬라이더를 실제 프로젝트에 그대로 옮겨 쓸 vanilla 코드로 받으려면 zip 비밀번호 nrfh573p 를 입력해 열어보면 됩니다. zip 안에는 아홉 개 모두 순수 html 범위 슬라이더 마크업과 vanilla js 뿐이라 프레임워크 없이 그대로 붙여 쓸 수 있습니다.

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

아홉 항목 모두 두 핸들에 role="slider"·aria-valuemin·aria-valuemax·aria-valuenow·aria-valuetext 를 붙여 지금 값을 스크린리더에 알립니다. 05 는 추가로 화면 밖 status 요소에 role="status"·aria-live="polite" 를 붙여 Tab 으로 어느 핸들에 초점이 있는지 문구로 읽어주고, 09 는 오른쪽 미리보기 패널 전체에 aria-hidden="true" 를 붙여 스크린리더가 중복된 값을 두 번 읽지 않게 막습니다. role="slider" 규격은 MDN slider role 문서를, prefers-reduced-motion 은 MDN prefers-reduced-motion 문서를 참고하면 됩니다.

변형 세 가지와 접근성

변형 값 느낌
최소 간격 값 낮추기 01 GAP 14 → 6 최소 간격 값을 낮추면 두 핸들을 더 가깝게까지 붙일 수 있게 되어 좁은 구간도 고를 수 있다.
스냅 눈금 간격 넓히기 07 STEP 10 → 25 스냅 눈금 간격을 넓히면 핸들이 멈출 수 있는 자리 수가 줄어 값이 더 성긴 단위로만 골라진다.
툴팁 합체 임계 거리 낮추기 03 MERGE_PX 46 → 18 툴팁 합체 임계 거리를 낮추면 두 값이 훨씬 더 가까워져야만 풍선 두 개가 하나로 합쳐진다.

세 변형 모두 상수 하나만 바뀔 뿐, pointerdown/keydown 리스너 구성이나 role="slider"·aria-valuenow·aria-valuetext 흐름은 손대지 않습니다. 스크린리더가 읽는 값의 형식도 수정 전후로 달라지지 않으므로 어떤 값을 골라도 접근성 지원 범위는 그대로 유지됩니다.

html 범위 슬라이더 예제는 어디서 보나?

이 편 위쪽 3×3 격자가 바로 그 예제이고, 항목마다 이어지는 코드 펜스를 열면 계산 부분만 따로 읽을 수 있습니다. 값 하나짜리 슬라이더까지 폭넓게 보려면 334, 목록 자체를 좁히는 필터 흐름까지 보려면 346 을 함께 열어보면 됩니다.

FAQ

레인지 슬라이더의 초기값을 값 1개가 아니라 2개로 바꾸려면 어떻게 하나요?

아홉 항목 모두 v.lo·v.hi(08 은 target·pos) 두 필드를 한 객체에 담아 관리합니다. HTML 의 aria-valuenow 초깃값과 JS 의 v.lo·v.hi 초깃값을 같은 숫자로 맞춰 바꾸면 됩니다.

두 핸들이 서로 넘어가지 않게 막는 값은 어디서 정하나요?

01 은 GAP, 08 은 MIN_GAP 이 그 값입니다. 01 은 초과分을 그 자리에서 막기만 하고, 08 은 초과하려는 순간 반대쪽 목표값도 같이 밀어내 두 핸들이 항상 MIN_GAP 이상 떨어져 있게 만든다는 점이 다릅니다.

reduced-motion을 켠 화면에서도 아홉 가지 범위 슬라이더가 전부 되나요?

네, 됩니다. reduce 가 막는 건 자동 시연 루프와 트랜지션 재생뿐이고, 드래그·방향키로 값을 바꾸는 동작 자체는 아홉 항목 모두 그대로 동작합니다. 08 은 스프링 계산까지 건너뛰고 목표값으로 즉시 이동해 오히려 더 단순하게 반응합니다.

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

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