Slider
범위 내 값을 선택하는 슬라이더 컴포넌트
목차
연속적인 값 또는 범위를 선택할 수 있는 슬라이더 컴포넌트입니다.
기본 사용법 (Basic Usage)#
// 기본 슬라이더
Slider(
value: volume,
onChanged: handleVolumeChange,
min: 0,
max: 100,
)
// 단계 지정
Slider(
value: rating,
onChanged: handleRatingChange,
min: 0,
max: 5,
divisions: 5,
)
// 비활성화
Slider(
value: volume,
enabled: false,
min: 0,
max: 100,
)
// 앞 · 뒤 슬롯
Slider(
value: volume,
onChanged: handleVolumeChange,
leading: Icon(LucideIcons.volume),
trailing: Icon(LucideIcons.volume2),
)
// 기본 슬라이더
Slider(
value: volume,
onChanged: handleVolumeChange,
min: 0,
max: 100,
)
// 단계(step) 지정
Slider(
value: rating,
onChanged: handleRatingChange,
min: 0,
max: 5,
step: 1,
)
// 비활성화
Slider(
value: volume,
enabled: false,
min: 0,
max: 100,
)
// 앞 · 뒤 슬롯
Slider(
value: volume,
onChanged: handleVolumeChange,
leading: Icon(LucideIcons.volume),
trailing: Icon(LucideIcons.volume2),
)
Live Preview#
코드 보기
class SliderDefaultExample extends StatefulComponent {
const SliderDefaultExample({super.key});
@override
State<SliderDefaultExample> createState() => _SliderDefaultExampleState();
}
class _SliderDefaultExampleState extends State<SliderDefaultExample> {
double _value = 50;
@override
Component build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (v) => setState(() => _value = v),
);
}
}
class SliderDefaultExample extends StatefulWidget {
const SliderDefaultExample({super.key});
@override
State<SliderDefaultExample> createState() => _SliderDefaultExampleState();
}
class _SliderDefaultExampleState extends State<SliderDefaultExample> {
double _value = 50;
@override
Widget build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (v) => setState(() => _value = v),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 볼륨, 밝기 등 연속적인 값을 직관적으로 조절할 때
- 가격 범위, 나이 범위 등 두 값 사이의 범위를 선택할 때
- 값의 상대적 위치를 시각적으로 확인하면서 조절할 때
대신 다른 컴포넌트를 사용하세요:
TextField: 정확한 숫자 입력이 필요할 때Progress: 사용자 조작 없이 진행률만 표시할 때Select: 불연속적인 옵션 중 선택할 때
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | double | 0.0 | 현재 값 |
onChanged |
ValueChanged<double>? |
null |
값 변경 콜백 |
onChangeStart |
ValueChanged<double>? |
null |
드래그 시작 콜백 |
onChangeEnd |
ValueChanged<double>? |
null |
드래그 종료 콜백 |
leading |
Widget? (Web Component?) |
null |
트랙 앞에 그리는 슬롯 — 예: 볼륨 작게 아이콘. 트랙과 slotGapStyle 만큼 떨어집니다 |
trailing |
Widget? (Web Component?) |
null |
트랙 뒤에 그리는 슬롯 — 예: 볼륨 크게 아이콘 |
min | double | 0.0 | 최솟값 |
max | double | 100.0 | 최댓값 |
step | double? | null | 단계 크기 |
divisions | int? | null | 단계 수 |
enabled | bool | true | 활성화 여부 |
sliderStyle |
CoreSliderStyle? |
null |
track + thumb chrome / nested tickLabelStyle 묶음 |
Web 은 위 파라미터에 더해 id 를 받아 루트 <div> 로 통과시킵니다.
빠른 오버라이드 (Chain)#
이미 만든 Slider 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 ==
CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class SliderChainExample extends StatefulWidget {
const SliderChainExample({super.key});
@override
State<SliderChainExample> createState() => _SliderChainExampleState();
}
class _SliderChainExampleState extends State<SliderChainExample> {
double _value = 50;
@override
Widget build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (v) => setState(() => _value = v),
)
.withStyle(
const CoreSliderStyle(
trackHeight: CoreSpace.space12,
thumbSize: CoreSpace.space24,
activeColor: CoreColor.token(CoreColors.tertiary),
),
)
.radius16;
}
}
class SliderChainExample extends StatefulComponent {
const SliderChainExample({super.key});
@override
State<SliderChainExample> createState() => _SliderChainExampleState();
}
class _SliderChainExampleState extends State<SliderChainExample> {
double _value = 50;
@override
Component build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (v) => setState(() => _value = v),
)
.withStyle(
const CoreSliderStyle(
trackHeight: CoreSpace.space12,
thumbSize: CoreSpace.space24,
activeColor: CoreColor.token(CoreColors.tertiary),
),
)
.radius16;
}
}
스타일 시스템 (Style System)#
sliderStyle 슬롯#
Slider 의 track + thumb chrome / 슬롯 미세 조정은 단일 sliderStyle
(CoreSliderStyle) 으로 흐릅니다. Slider 는 시맨틱 enum (variant) 이 없어
behaviour 필드만 위젯 파라미터.
Slider(
value: temperature,
min: 0,
max: 100,
divisions: 10,
onChanged: handleChange,
sliderStyle: CoreSliderStyle(
trackColor: CoreColor.token(CoreColors.surfaceContainer),
activeColor: CoreColor.token(CoreColors.primary),
trackHeight: CoreSpace.space6,
thumbColor: CoreColor.token(CoreColors.surface),
thumbBorderColor: CoreColor.token(CoreColors.primary),
thumbSize: CoreSpace.space20,
borderRadius: CoreBorderRadius.all(CoreRadius.radius9999),
containerPadding: CoreEdgeInsets.symmetric(vertical: CoreSpace.space8),
tickLabelStyle: CoreTextStyle.token(CoreTextStyles.bodySmall),
),
)
CoreSliderStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
trackColor | CoreColor? | Inactive track fill colour. |
activeColor |
CoreColor? |
Active (filled) track fill colour. |
trackHeight |
double? |
Track height override (logical px). |
disabledTrackColor |
CoreColor? |
Inactive track fill colour when the slider is disabled. |
disabledActiveColor |
CoreColor? |
Active (filled) track colour when disabled. |
disabledThumbColor |
CoreColor? |
Thumb background fill colour when disabled. |
disabledThumbBorderColor |
CoreColor? |
Thumb border stroke colour when disabled. |
containerPadding |
CoreEdgeInsets? |
Padding inside the slider container — provides hit-area room above / below the track (symmetric vertical padding by default). |
borderRadius |
CoreBorderRadius? |
Corner radius override for the track / fill / thumb.
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). |
thumbShadow |
List<CoreShadowLayer>? |
Thumb drop-shadow override. null defers to [defaultThumbShadow]. |
slotGapStyle |
CoreGapStyle? |
Gap between the track and each of the leading / trailing slots.
null
defers to [defaultSlotGapStyle].
|
tickLabelStyle |
CoreTextStyle? |
Tick label text style override (when divisions / discrete labels are rendered). |
focusOutlineStyle |
CoreFocusOutlineStyle? |
Keyboard focus ring override, forwarded to the composed
FocusOutline
.
null
defers to [defaultFocusOutlineStyle], and any field left null inside it defers to
FocusOutline
's own resolver.
|
Resolve chain#
design system default
→ CoreSliderTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.sliderStyle // 인스턴스별
nested tickLabelStyle 은 자기 컴포넌트의 resolve chain 으로 다시 한 번 머지됩니다.
변형 (Variants)#
단계 지정#
Slider(
value: temperature,
onChanged: handleTempChange,
min: 16,
max: 30,
divisions: 14,
)
Step 지정#
Slider(
value: rating,
onChanged: handleRatingChange,
min: 0,
max: 5,
step: 1,
)
동작 스펙 (Behavior)#
값 제어#
Slider(
value: 50,
min: 0,
max: 100,
onChanged: (v) => setState(() => _value = v),
)
-
Slider 는 controlled 컴포넌트입니다 —
value를 그대로 그리고, 변경은onChanged로만 알립니다. 상태는 호출자가 소유합니다. onChanged가null이면 표시 전용으로 동작합니다.
드래그 인터랙션#
- 트랙 아무 곳이나 누르면 thumb 가 그 위치로 이동하고, 이어서 드래그할 수 있습니다.
- 드래그 시작 시
onChangeStart, 종료 시onChangeEnd가 호출됩니다. - 단일 값 전용입니다 — 두 개의 thumb 를 가진 범위 모드는 없으므로, 범위 필터는 Slider 두 개를 나란히 두고 각자의 값을 관리합니다.
Divisions / step (스냅)#
Slider(
value: rating,
onChanged: handleRating,
min: 0, max: 5, divisions: 5,
)
divisions가 설정되면 값이 가장 가까운 단계에 스냅되고 tick 마크가 표시됩니다.step은 스냅 간격을 값 단위로 직접 지정합니다 (divisions대신 사용).
기본 치수#
| 항목 | 기본값 | Core 토큰 |
|---|---|---|
| 트랙 높이 | 4 | CoreSpace.space4 |
| thumb 크기 | 16 | CoreSpace.space16 |
| thumb 보더 두께 | 1 | CoreStrokeWidth.stroke1 |
| 모서리 반경 | pill | CoreRadius.radius9999 |
| 컨테이너 패딩 | 상하 8 | CoreSpace.space8 |
| thumb 그림자 | 가장 작은 단계 | CoreShadow.xs |
| 슬롯 간격 | 12 | CoreSpace.space12 |
비활성화 상태는 알파를 곱하지 않고 전용 색 토큰
(disabledTrackColor / disabledActiveColor / disabledThumbColor
/
disabledThumbBorderColor) 으로 표현합니다.
앞 · 뒤 슬롯#
leading / trailing 은 트랙 앞뒤에 임의의 위젯을 그립니다. 슬롯이 null 이면
아무것도 그리지 않고 간격도 없습니다 — 슬롯 없는 슬라이더의 배치는 그대로입니다.
슬롯과 트랙 사이는 sliderStyle.slotGapStyle (CoreGapStyle, 기본 12) 입니다.
슬롯은 호출자의 위젯입니다 — 텍스트 슬롯은 Text(...) 체인으로 스타일을 주세요. (Flutter 는 슬라이더 전체를
tickLabelStyle 의 DefaultTextStyle 로 감싸므로 스타일 없는 슬롯 글자가 그 역할을 상속합니다.) 슬롯을 눌러도 값이 바뀌지 않습니다; 포인터는
트랙 위에서만 값에 매핑됩니다.
포커스 링#
키보드로 포커스를 받으면 공통 FocusOutline 링이 thumb 둘레에 나타납니다
(트랙 전체가 아닙니다). 마우스·터치로 잡았을 때는 나타나지 않습니다.
플랫폼별 구현#
양 플랫폼 모두 네이티브 슬라이더 컨트롤을 쓰지 않고 트랙 / fill / thumb 을 직접 그리며, 포인터(탭·드래그)와 키보드(화살표/Page/Home/End, "키보드 인터랙션" 절 참조) 양쪽으로 값을 바꿀 수 있습니다.
-
Flutter:
GestureDetector(onTapDown+onHorizontalDrag*) +MouseRegion으로 탭·드래그를 받고, 트랙 / 활성 트랙 / thumb 을Container+Stacks로 그립니다. 슬롯이 있으면Row안에서 트랙이Expanded가 됩니다.FocusableActionDetector가 키보드 shortcuts/actions 과 포커스 상태를 관리합니다. 접근성은Semantics(slider: true, value: ...)입니다. -
Web: 루트가
<div>이며 (<input type="range">는 쓰지 않습니다)pointerdown에서 document 드래그 리스너를 붙여 값을 갱신하고,keydown이 화살표/Page/Home/End 를 처리합니다.role="slider",tabindex="0"(활성 시),aria-valuemin/aria-valuemax/aria-valuenow는 컴포넌트가 직접 emit 하며, 비활성 시aria-disabled="true"가 추가됩니다.
사용 가이드라인 (Usage Guidelines)#
하나의 연속·단계 값을 조절하려면 Slider를 사용합니다. 최소와 최대를 함께 고르는 구간은 Range, 조작 없이 진행률을 표시하는 것은 Progress, 별 모양 점수는 StarRating을 선택합니다. min/max/value/step은 같은 사용자 값 단위이며 트랙 길이의 논리 px와 구분합니다.
✅ Do#
슬라이더에 현재 값을 라벨로 표시하세요.
Slider(
value: temperature,
onChanged: handleTemp,
min: 16, max: 30,
divisions: 14,
)
사용자가 정확한 값을 확인하면서 조절할 수 있습니다.
❌ Don't#
정밀한 값 입력에 슬라이더만 사용하지 마세요.
// ❌ Bad — 정확한 금액 입력이 어려움
Slider(value: price, min: 0, max: 1000000, onChanged: handlePrice)
// ✅ Good — 슬라이더 + 입력 필드 조합
Row(children: [
Expanded(child: Slider(value: price, min: 0, max: 1000000, onChanged: handlePrice)),
Gap(size: CoreSpace.space16),
SizedBox(
width: CoreSpace.space80,
child: TextField(initialValue: price.toString()),
),
])
✅ Do#
범위 슬라이더로 필터를 구현하세요.
Slider(
value: minPrice,
onChanged: handleMinPriceFilter,
min: 0,
max: 500000,
step: 10000,
)
Slider(
value: maxPrice,
onChanged: handleMaxPriceFilter,
min: 0,
max: 500000,
step: 10000,
)
가격, 나이 등 범위 필터에 직관적입니다.
❌ Don't#
3개 이상의 값이 필요하면 슬라이더를 사용하지 마세요.
슬라이더는 1~2개 값(단일/범위)에 최적화되어 있습니다. 복잡한 입력은 개별 필드를 사용하세요.
✅ Do#
이산 값에는 divisions를 설정하세요.
// 별점 1~5
Slider(
value: stars, min: 1, max: 5, divisions: 4,
onChanged: handleStars,
)
연속 값이 아닌 경우 스냅으로 정확한 선택을 보장합니다.
접근성 (Accessibility)#
역할#
-
Flutter:
Semantics(slider: true, value: '<현재 값>')하나만 붙습니다 — 라벨도, 최소·최대값도, 비활성 플래그도 실리지 않습니다. -
Web: 루트
<div>에 컴포넌트가role="slider"와aria-valuemin/aria-valuemax/aria-valuenow를 직접 emit 합니다 (네이티브<input type="range">가 자동으로 주는 것이 아니라<div>커스텀 렌더입니다).enabled: false면aria-disabled="true"가 붙고,enabled: true이면서onChanged가 있을 때만tabindex="0"이 붙습니다.
접근 가능한 이름은 어느 쪽에도 없습니다 — aria-label 도 Semantics(label:) 도 설정되지 않습니다.
키보드#
enabled: true 이면서 onChanged 가 있을 때만 포커스를 받고 아래 키가 동작합니다. 양 플랫폼이 같은 키 집합을 처리합니다.
| 키 | 동작 |
|---|---|
→ / ↑ | 한 스텝 증가 |
← / ↓ | 한 스텝 감소 |
PageUp / PageDown | 10 스텝 단위로 증가 / 감소 |
Home / End | min / max 로 점프 |
Tab / Shift+Tab | 다음 / 이전 포커스 가능 요소로 이동 |
한 스텝의 크기는 step → (max - min) / divisions → (max - min) 의 1% 순으로
결정됩니다(둘 다 없으면 1%, 네이티브 <input type="range"> 의 기본 동작과 동일).
Flutter 는 FocusableActionDetector 의 shortcuts 로, Web 은 루트의 keydown
핸들러로 처리합니다. Web 은 처리한 키마다 preventDefault 를 호출해 페이지
스크롤을 막습니다.
스크린 리더#
- 이름: 없습니다(위 참조). 리더는 역할과 값만 읽습니다.
-
값: Flutter 는
Semantics(value:)에'50.0'같은 원시double문자열을, Web 은aria-valuenow에 같은 문자열을 넣습니다.aria-valuetext는 없으므로 "50%" 처럼 단위를 붙인 표현으로 대체할 수 없습니다. -
통보: 라이브 리전이 아닙니다. 슬라이더에 포커스가 없는 동안
value가 바뀌면 통보되지 않습니다.
포커스 관리#
오버레이가 아니므로 열림/닫힘이 없고, 진입 시 포커스가 자동으로 옮겨지지 않으며
트랩도 없습니다. Tab 으로 들어오고 Tab 으로 나갑니다.
포인터로 트랙을 누르면 Flutter 는 _focusFromPointer() 로 슬라이더에 포커스를
가져오되 링은 감춥니다 — 드래그로 대략 맞춘 뒤 화살표 키로 미세 조정하는 흐름이
별도 Tab 없이 이어집니다. Web 은 pointerdown 에서 링만 끄고, 포커스 자체는
tabindex="0" 요소에 대한 브라우저 기본 동작에 맡깁니다.
포커스 링은 양 플랫폼 모두 FocusOutline 컴포넌트가 thumb 둘레에 그리고, 키보드 유래 포커스에만
나타납니다 — Flutter 는 하이라이트 콜백과 포인터 출처를 함께 보고, Web 은
:focus-visible 매치 결과를 읽습니다. 커스터마이즈는 sliderStyle
의
focusOutlineStyle (CoreFocusOutlineStyle) 로 합니다 — 색·두께뿐 아니라
align · borderRadius · offsetColor
· duration 까지 같은 슬롯에서 정해지고,
지정하지 않은 필드는 FocusOutline 의 기본값을 따릅니다.
Slider(
value: 50,
onChanged: (v) => setState(() => _value = v),
sliderStyle: const CoreSliderStyle(
focusOutlineStyle: CoreFocusOutlineStyle(
borderColor: CoreColor.token(CoreColors.error),
),
),
)
알려진 제약#
-
슬라이더에 접근 가능한 이름이 없습니다. 조건: 모든 인스턴스. 리더는 "슬라이더,
50" 처럼 무엇을 조절하는지 없이 읽습니다. 대안 — Flutter 는 호출부에서
Semantics(label:)로 감싸는 우회가 있지만, Web 은 아래 이유로 진입점 자체가 없습니다. -
Web
Slider에는 caller passthrough 가 없습니다. 생성자가super.key와super.id만 선언해attributes·classes·css· 이벤트 파라미터를 받지 않습니다.copyWith는attributes/classes/css를 인자로 받지만 생성자에 넘기지 않고 버립니다. 따라서aria-label·aria-labelledby·aria-describedby를 밖에서 실을 수 없습니다. 대안: 없음. -
Flutter 에는 스크린 리더 증감 액션이 등록되지 않습니다.
Semantics에onIncrease/onDecrease가 없어 VoiceOver·TalkBack 의 슬라이더 조정 제스처가 값을 바꾸지 못합니다(하드웨어 키보드 화살표는 위 표대로 동작합니다). 대안: 없음 — 해당 콜백을 받을 파라미터가 노출되어 있지 않습니다. -
Flutter 는
enabled: false를 시맨틱으로 알리지 않습니다. Web 은aria-disabled="true"를 붙이지만 Flutter 의Semantics에는enabled플래그가 없어, 비활성 슬라이더가 활성처럼 읽힙니다(양쪽 다 포커스는 받지 않습니다). 대안: 없음.
크로스 플랫폼 차이점 (Platform Differences)#
생성자 파라미터는 양 플랫폼 동일합니다 (Web 만 id 추가; 슬롯 타입만 Widget? ↔ Component?). 아래는 플랫폼 고유
차이점만 나열합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 트랙 렌더링 | Container + Stacks (네이티브 슬라이더 아님) |
<div> 3개 (track / fill / thumb, 네이티브 <input> 아님) |
| 포인터 입력 | GestureDetector (onTapDown + onHorizontalDrag*) |
pointerdown + document 드래그 리스너 |
| 접근성 정보 | Semantics(slider: true, value:) — 현재 값만 |
role="slider"
+
aria-valuemin
/
max
/
now
를 직접 emit
|
| 키보드 | FocusableActionDetector (shortcuts/actions) |
tabindex="0" + keydown 핸들러 |
| 슬롯 배치 | Row + Expanded(트랙) + Gap |
루트 flex 행 + 트랙 min-w-0 + Gap; 포인터는 트랙 요소 기준 |
| 포커스 링 | thumb 을 감싼 FocusOutline |
thumb 을 감싼 FocusOutline (위치는 감싼 호스트가 가짐) |
| chrome 진입점 | sliderStyle (CoreSliderStyle) |
sliderStyle (CoreSliderStyle) |
관련 컴포넌트 (Related Components)#
조합 예제#
// 가격 범위 필터 패턴
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: CoreSpace.space8,
children: [
Text('가격 범위'),
Slider(
value: minPrice,
onChanged: (v) => setState(() => minPrice = v),
min: 0,
max: 500000,
step: 10000,
),
Slider(
value: maxPrice,
onChanged: (v) => setState(() => maxPrice = v),
min: 0,
max: 500000,
step: 10000,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('₩${minPrice.toInt()}'),
Text('₩${maxPrice.toInt()}'),
],
),
],
)