Slider | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (When to Use)#

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

  • 볼륨, 밝기 등 연속적인 값을 직관적으로 조절할 때
  • 가격 범위, 나이 범위 등 두 값 사이의 범위를 선택할 때
  • 값의 상대적 위치를 시각적으로 확인하면서 조절할 때

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

  • TextField: 정확한 숫자 입력이 필요할 때
  • Progress: 사용자 조작 없이 진행률만 표시할 때
  • Select: 불연속적인 옵션 중 선택할 때

Props / Parameters#

속성타입기본값설명
valuedouble0.0현재 값
onChanged ValueChanged<double>? null 값 변경 콜백
onChangeStart ValueChanged<double>? null 드래그 시작 콜백
onChangeEnd ValueChanged<double>? null 드래그 종료 콜백
leading Widget? (Web Component?) null 트랙 앞에 그리는 슬롯 — 예: 볼륨 작게 아이콘. 트랙과 slotGapStyle 만큼 떨어집니다
trailing Widget? (Web Component?) null 트랙 뒤에 그리는 슬롯 — 예: 볼륨 크게 아이콘
mindouble0.0최솟값
maxdouble100.0최댓값
stepdouble?null단계 크기
divisionsint?null단계 수
enabledbooltrue활성화 여부
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 필드#

필드타입설명
trackColorCoreColor?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 토큰
트랙 높이4CoreSpace.space4
thumb 크기16CoreSpace.space16
thumb 보더 두께1CoreStrokeWidth.stroke1
모서리 반경pillCoreRadius.radius9999
컨테이너 패딩상하 8CoreSpace.space8
thumb 그림자가장 작은 단계CoreShadow.xs
슬롯 간격12CoreSpace.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 / PageDown10 스텝 단위로 증가 / 감소
Home / Endmin / 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?). 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
트랙 렌더링 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)
  • Progress: 진행률 표시. Slider는 입력, Progress는 출력
  • TextField: 정확한 숫자 입력. Slider와 함께 사용 가능

조합 예제#

// 가격 범위 필터 패턴
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()}'),
      ],
    ),
  ],
)