Slider | CoUI
LogoCoUI

Slider

범위 내 값을 선택하는 슬라이더 컴포넌트

Slider#

연속적인 값 또는 범위를 선택할 수 있는 슬라이더 컴포넌트입니다.

Live Preview#

사용 시기 (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#

속성타입기본값설명
valuedouble0.0현재 값
onChanged ValueChanged<double>? null 값 변경 콜백
onChangeStart ValueChanged<double>? null 드래그 시작 콜백
onChangeEnd ValueChanged<double>? 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;
  }
}

스타일 시스템 — 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 / 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 로만 알립니다. 상태는 호출자가 소유합니다.
  • onChangednull 이면 표시 전용으로 동작합니다.

드래그 인터랙션#

  • 트랙 아무 곳이나 누르면 thumb 가 그 위치로 이동하고, 이어서 드래그할 수 있습니다.
  • 드래그 시작 시 onChangeStart, 종료 시 onChangeEnd 가 호출됩니다.
  • 단일 값 전용입니다 — 두 개의 thumb 를 가진 범위 모드는 없으므로, 범위 필터는 Slider 두 개를 나란히 두고 각자의 값을 관리합니다.

Divisions / step (스냅)#

Slider(
  value: rating,
  onChanged: handleRating,
  min: 0, max: 5, divisions: 5,
)
  • divisions 가 설정되면 값이 가장 가까운 단계에 스냅되고 tick 마크가 표시됩니다.
  • step 은 스냅 간격을 값 단위로 직접 지정합니다 (divisions 대신 사용).

기본 치수#

항목기본값Core 토큰
트랙 높이6CoreSpace.space6
thumb 크기20CoreSpace.space20
thumb 보더 두께2CoreStrokeWidth.stroke2
모서리 반경pillCoreRadius.radius9999
컨테이너 패딩상하 8CoreSpace.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 / Endmin / max 로 점프

한 스텝의 크기는 step(max - min) / divisions(max - min) 의 1% 순으로 결정됩니다(둘 다 없으면 1%, 네이티브 <input type="range"> 의 기본 동작과 동일). 포커스 링은 FocusOutline 컴포넌트가 그리므로, sliderStylefocusOutlineStyle (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: falsearia-disabled="true" 가 붙습니다.

두 플랫폼이 전달하는 정보량이 다릅니다 — Web 은 범위까지, Flutter 는 현재 값만. 슬라이더가 나타내는 범위를 라벨로도 노출하면 이 차이에 기대지 않아도 됩니다.

터치 영역#

  • containerPadding 이 thumb 위아래로 여유를 만들어 트랙보다 넓은 터치 영역을 확보합니다.
  • 트랙 전체가 클릭 가능하므로 thumb 를 정확히 짚지 않아도 값을 바꿀 수 있습니다.

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

생성자 파라미터는 양 플랫폼 동일합니다 (Web 만 id 추가). 아래는 플랫폼 고유 차이점만 나열합니다.

항목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 핸들러
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()}'),
      ],
    ),
  ],
)