Range | CoUI
LogoCoUI

Range

두 개의 핸들로 값 구간을 선택하는 듀얼 핸들 슬라이더

Range#

두 개의 핸들로 [minValue, maxValue] 구간을 선택하는 듀얼 핸들 슬라이더입니다. 단일 값을 선택하는 Slider와 달리, 가격대·날짜 범위·필터 구간 같은 범위 선택에 사용합니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 최소/최대 두 값으로 구간을 선택받는 경우 (가격대 필터 등)
  • 핸들 사이 구간을 시각적으로 강조해야 하는 경우

대신 다른 컴포넌트를 사용하세요:

  • Slider: 단일 값을 선택하는 경우

기본 사용법 (Basic Usage)#

// 기본 범위 슬라이더
Range(
  minValue: minVal,
  maxValue: maxVal,
  onChanged: (min, max) => setState(() {
    minVal = min;
    maxVal = max;
  }),
)

// 구간 / 분할 조정
Range(
  minValue: 25,
  maxValue: 75,
  min: 0,
  max: 200,
  divisions: 20,
  onChanged: (min, max) => updateRange(min, max),
)
// 기본 범위 슬라이더
Range(
  minValue: minVal,
  maxValue: maxVal,
  onChanged: (min, max) => setState(() {
    minVal = min;
    maxVal = max;
  }),
)

// 구간 / 분할 조정
Range(
  minValue: 25,
  maxValue: 75,
  min: 0,
  max: 200,
  divisions: 20,
  onChanged: (min, max) => updateRange(min, max),
)

빠른 오버라이드 (Chain)#

이미 만든 Range 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class RangeChainExample extends StatefulWidget {
  const RangeChainExample({super.key});

  @override
  State<RangeChainExample> createState() => _RangeChainExampleState();
}

class _RangeChainExampleState extends State<RangeChainExample> {
  double _min = 20;
  double _max = 80;

  @override
  Widget build(BuildContext context) {
    return Range(
          minValue: _min,
          maxValue: _max,
          onChanged: (min, max) => setState(() {
            _min = min;
            _max = max;
          }),
        )
        .withStyle(
          const CoreRangeStyle(
            trackHeight: CoreSpace.space12,
            thumbSize: CoreSpace.space24,
            activeTrackColor: CoreColor.token(CoreColors.tertiary),
          ),
        )
        .radius16;
  }
}
class RangeChainExample extends StatefulComponent {
  const RangeChainExample({super.key});

  @override
  State<RangeChainExample> createState() => _RangeChainExampleState();
}

class _RangeChainExampleState extends State<RangeChainExample> {
  double _min = 20;
  double _max = 80;

  @override
  Component build(BuildContext context) {
    return Range(
          minValue: _min,
          maxValue: _max,
          onChanged: (min, max) => setState(() {
            _min = min;
            _max = max;
          }),
        )
        .withStyle(
          const CoreRangeStyle(
            trackHeight: CoreSpace.space12,
            thumbSize: CoreSpace.space24,
            activeTrackColor: CoreColor.token(CoreColors.tertiary),
          ),
        )
        .radius16;
  }
}

Props / Parameters#

속성타입기본값설명
minValue double 0.0 하단(최소) 핸들의 현재 값
maxValue double 100.0 상단(최대) 핸들의 현재 값
mindouble0.0트랙의 절대 최소값
maxdouble100.0트랙의 절대 최대값
stepdouble?null값 증가 단위
divisions int? null 트랙의 이산 분할 개수
enabledbooltrue상호작용 가능 여부
onChanged void Function(double, double)? null 값 변경 콜백 (minValue, maxValue)
leading Widget? / Component? null 슬라이더 앞에 표시할 위젯
trailing Widget? / Component? null 슬라이더 뒤에 표시할 위젯
rangeStyle CoreRangeStyle? null 트랙 색상 / 두께 / 핸들 / 간격 오버라이드

스타일 시스템 (Style System)#

CoreRangeStyle 필드#

필드타입설명
activeTrackColor CoreColor? Active (selected, between-thumbs) track fill colour.
inactiveTrackColor CoreColor? Inactive (outside-selection) track fill colour.
trackHeight double? Track height override (logical px).
borderRadius CoreBorderRadius? Corner radius override for the track / fill / thumbs. null → [defaultBorderRadius] (full pill).
thumbColor CoreColor? Thumb background fill colour.
thumbBorderColor CoreColor? Thumb border stroke colour.
thumbSize double? Thumb size (width = height) override (logical px).
thumbBorderWidth double? Thumb border stroke width override (logical px). Defaults to [defaultThumbBorderWidth].
minMaxGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the slider and its leading / trailing Gap siblings (min-side / max-side affordance). Forwarded to the Gap siblings flanking the slider (Flutter) or used to drive the root row gap CSS (Web). Merged on top of [defaultMinMaxGapStyle].
focusOutlineStyle CoreFocusOutlineStyle? Keyboard focus ring override, forwarded to the FocusOutline the focused thumb composes. null defers to [defaultFocusOutlineStyle], and any field left null inside it defers to FocusOutline 's own resolver.

동작 스펙 (Behavior)#

인터랙션#

  • 드래그: 드래그 시작 지점에서 가까운 핸들이 선택되어 이동
  • : 탭한 위치에서 가까운 핸들이 그 지점으로 이동
  • 핸들끼리 교차하지 않도록 minValue ≤ maxValue 가 항상 유지됨
  • enabledfalse이거나 onChangednull이면 비활성

사용 가이드라인 (Usage Guidelines)#

✅ Do#

정확한 단위가 필요하면 step 을 명시적으로 지정

Range(
  minValue: 25,
  maxValue: 75,
  min: 0,
  max: 200,
  step: 5,
  onChanged: (min, max) => updateRange(min, max),
)

한 스텝의 크기는 step(max - min) / divisions(max - min) 의 1% 순으로 결정됩니다. 화살표 키 조작 단위를 정확히 통제하려면 divisions 대신 step 을 직접 지정하는 편이 의도를 명확히 드러냅니다.


❌ Don't#

stepdivisions를 동시에 지정하고 divisions가 적용될 거라 기대하지 않기

// ❌ step 이 있으면 divisions 는 무시됨 — 20 분할이 아니라 5 단위로 움직임
Range(
  minValue: 25,
  maxValue: 75,
  min: 0,
  max: 200,
  step: 5,
  divisions: 20,
  onChanged: (min, max) => updateRange(min, max),
)

step이 설정되면 divisions보다 우선합니다. 두 값을 모두 주면 어느 쪽이 실제로 적용되는지 코드만 보고는 알 수 없어 혼란을 줍니다 — 하나만 선택하세요.

접근성 (Accessibility)#

역할 / Semantics#

두 핸들이 각자 독립적인 role="slider" 입니다 — 트랙 자체는 시맨틱이 없는 순수 시각 요소입니다(ARIA APG "Slider (Multi-Thumb)" 패턴). 하나의 결합된 tabstop 으로는 aria-valuenow 가 요구하는 단일 숫자에 두 값을 담을 수 없고, 화살표 키로 "어느 쪽을 움직이는지" 표현할 방법도 없기 때문입니다.

  • aria-valuenow — 각 핸들의 현재 값(숫자)
  • aria-valuemin / aria-valuemax — 상호 제약: min 핸들의 상한은 max 핸들의 현재 값, max 핸들의 하한은 min 핸들의 현재 값 (서로를 앞지를 수 없음)
  • aria-label — "최솟값" / "최댓값" (로케일 문자열, CouiLocalizations / CoUILocalizationsrangeMinValueLabel / rangeMaxValueLabel)
  • 비활성 시 각 핸들에 aria-disabled="true"

키보드 인터랙션#

활성 상태일 때 각 핸들을 Tab 으로 개별 포커스할 수 있고, 포커스된 핸들에 한해:

  • / — 한 스텝 증가, / — 한 스텝 감소
  • PageUp / PageDown — 10 스텝 단위로 증가 / 감소
  • Home / End — 그 핸들의 하한 / 상한으로 점프 (전역 min/max 가 아니라 상호 제약 경계 — 예: min 핸들의 End 는 max 핸들의 현재 값으로 이동)

한 스텝의 크기는 step(max - min) / divisions(max - min) 의 1% 순으로 결정됩니다 (Slider 와 동일 규칙). 트랙을 드래그하거나 탭하면 가장 가까운 핸들이 선택되고 그 핸들에 키보드 포커스도 함께 옮겨져, 드래그로 대략 맞춘 뒤 화살표 키로 미세 조정하는 흐름이 별도 Tab 없이 이어집니다.

포커스 링은 FocusOutline 컴포넌트가 그리므로 rangeStylefocusOutlineStyle (CoreFocusOutlineStyle) 로 커스터마이즈합니다 — 색·두께뿐 아니라 align · offsetColor · duration 까지 같은 슬롯에서 정해지고, 지정하지 않은 필드는 FocusOutline 의 기본값을 따릅니다. 핸들은 원형이라 링의 모양은 CoreFocusOutlineShape.circle 에서 옵니다.

Range(
  minValue: 20,
  maxValue: 80,
  onChanged: (min, max) => setState(() { /* ... */ }),
  rangeStyle: const CoreRangeStyle(
    focusOutlineStyle: CoreFocusOutlineStyle(
      borderColor: CoreColor.token(CoreColors.error),
    ),
  ),
)

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명RangeRange
드래그 GestureDetector 수평 드래그 mousedown + document drag listener
핸들 렌더링 <div> 동등 Container <div>
키보드 핸들마다 FocusableActionDetector + FocusNode 핸들마다 tabindex="0" + keydown
  • Slider: 단일 값 선택 슬라이더