Slider#
연속적인 값 또는 범위를 선택할 수 있는 슬라이더 컴포넌트입니다.
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),
);
}
}
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;
}
}
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;
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 볼륨, 밝기 등 연속적인 값을 직관적으로 조절할 때
- 가격 범위, 나이 범위 등 두 값 사이의 범위를 선택할 때
- 값의 상대적 위치를 시각적으로 확인하면서 조절할 때
대신 다른 컴포넌트를 사용하세요:
TextField: 정확한 숫자 입력이 필요할 때Progress: 사용자 조작 없이 진행률만 표시할 때Select: 불연속적인 옵션 중 선택할 때
기본 사용법 (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,
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,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | double | 0.0 | 현재 값 |
onChanged |
ValueChanged<double>? |
null |
값 변경 콜백 |
onChangeStart |
ValueChanged<double>? |
null |
드래그 시작 콜백 |
onChangeEnd |
ValueChanged<double>? |
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;
}
}
스타일 시스템 — 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 / thumb-border 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). |
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 토큰 |
|---|---|---|
| 트랙 높이 | 6 | CoreSpace.space6 |
| thumb 크기 | 20 | CoreSpace.space20 |
| thumb 보더 두께 | 2 | CoreStrokeWidth.stroke2 |
| 모서리 반경 | pill | CoreRadius.radius9999 |
| 컨테이너 패딩 | 상하 8 | CoreSpace.space8 |
비활성화 상태는 알파를 곱하지 않고 전용 색 토큰
(disabledTrackColor / disabledActiveColor) 으로 표현합니다.
플랫폼별 구현#
양 플랫폼 모두 네이티브 슬라이더 컨트롤을 쓰지 않고 트랙 / fill / thumb 을 직접 그리며, 포인터(탭·드래그)와 키보드(화살표/Page/Home/End, "키보드 인터랙션" 절 참조) 양쪽으로 값을 바꿀 수 있습니다.
-
Flutter:
GestureDetector(onTapDown+onHorizontalDrag*) +MouseRegion으로 탭·드래그를 받고, 트랙 / 활성 트랙 / thumb 을Container+Stacks로 그립니다.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)#
✅ 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)#
키보드 인터랙션#
활성 상태(enabled: true + onChanged 있음)일 때 양 플랫폼 모두 키보드로 조작할
수 있습니다 — 탭/클릭하거나 Tab 으로 포커스를 옮긴 뒤:
→/↑— 한 스텝 증가,←/↓— 한 스텝 감소PageUp/PageDown— 10 스텝 단위로 증가 / 감소Home/End—min/max로 점프
한 스텝의 크기는 step → (max - min) / divisions → (max - min) 의 1% 순으로
결정됩니다(둘 다 없으면 1%, 네이티브 <input type="range"> 의 기본 동작과 동일).
포커스 링은 FocusOutline 컴포넌트가 그리므로, 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),
),
),
)
스크린 리더#
-
Flutter:
Semantics(slider: true, value: ...)로 현재 값만 전달됩니다. 최솟값·최댓값은 시맨틱에 실리지 않습니다. -
Web: 루트
<div>에 컴포넌트가role="slider"와aria-valuemin/aria-valuemax/aria-valuenow를 직접 emit 합니다 (네이티브<input type="range">가 자동으로 주는 것이 아닙니다).enabled: false면aria-disabled="true"가 붙습니다.
두 플랫폼이 전달하는 정보량이 다릅니다 — Web 은 범위까지, Flutter 는 현재 값만. 슬라이더가 나타내는 범위를 라벨로도 노출하면 이 차이에 기대지 않아도 됩니다.
터치 영역#
containerPadding이 thumb 위아래로 여유를 만들어 트랙보다 넓은 터치 영역을 확보합니다.- 트랙 전체가 클릭 가능하므로 thumb 를 정확히 짚지 않아도 값을 바꿀 수 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
생성자 파라미터는 양 플랫폼 동일합니다 (Web 만 id 추가). 아래는 플랫폼 고유
차이점만 나열합니다.
| 항목 | 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 핸들러 |
| 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()}'),
],
),
],
)