Range#
두 개의 핸들로 [minValue, maxValue] 구간을 선택하는 듀얼 핸들 슬라이더입니다. 단일 값을 선택하는 Slider와 달리, 가격대·날짜 범위·필터 구간 같은
범위 선택에 사용합니다.
Live Preview#
class RangeDefaultExample extends StatefulComponent {
const RangeDefaultExample({super.key});
@override
State<RangeDefaultExample> createState() => _RangeDefaultExampleState();
}
class _RangeDefaultExampleState extends State<RangeDefaultExample> {
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;
}),
);
}
}
class RangeDefaultExample extends StatefulWidget {
const RangeDefaultExample({super.key});
@override
State<RangeDefaultExample> createState() => _RangeDefaultExampleState();
}
class _RangeDefaultExampleState extends State<RangeDefaultExample> {
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;
}),
);
}
}
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;
}
}
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;
}
}
사용 시기 (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 |
상단(최대) 핸들의 현재 값 |
min | double | 0.0 | 트랙의 절대 최소값 |
max | double | 100.0 | 트랙의 절대 최대값 |
step | double? | null | 값 증가 단위 |
divisions |
int? |
null |
트랙의 이산 분할 개수 |
enabled | bool | true | 상호작용 가능 여부 |
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가 항상 유지됨 enabled가false이거나onChanged가null이면 비활성
사용 가이드라인 (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#
step과 divisions를 동시에 지정하고 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/CoUILocalizations의rangeMinValueLabel/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 컴포넌트가 그리므로 rangeStyle 의
focusOutlineStyle (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)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Range | Range |
| 드래그 | GestureDetector 수평 드래그 |
mousedown + document drag listener |
| 핸들 렌더링 | <div> 동등 Container |
<div> |
| 키보드 | 핸들마다 FocusableActionDetector + FocusNode |
핸들마다 tabindex="0" + keydown |
관련 컴포넌트 (Related Components)#
- Slider: 단일 값 선택 슬라이더