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

Progress

진행률 표시 컴포넌트

목차

작업 진행률을 시각적으로 표시하는 컴포넌트입니다. 직선형(Progress)과 원형(RadialProgress)을 지원합니다.

기본 사용법 (Basic Usage)#

// 기본 진행률 바 (0.0 ~ 1.0 범위)
Progress(value: 0.65)

// 커스텀 min/max 범위 지정
Progress(value: 75, min: 0, max: 100)

// 채움이 무엇을 뜻하는지는 tone 으로 — 트랙은 모든 tone 에서 같은 중립색
Progress(value: 0.92, tone: CoreProgressTone.warning)

// 막대 위 라벨 · 값, 막대 아래 캡션 (모두 선택)
Progress(
  value: 0.62,
  label: Text('Team plan'),
  valueLabel: Text('62%'),
  caption: Text('AI usage · 18 days left'),
)

// 단계형 — 단계 수만큼 나뉜 막대. 지나온 단계는 채움 색, 남은 단계는 트랙 색
Progress(
  value: 1,
  max: 2,
  segments: 2,
  label: Text('STEP 1 / 2'),
)

// 색상/두께 커스터마이징은 progressStyle 슬롯으로
Progress(
  value: 0.5,
  progressStyle: CoreProgressStyle(
    color: CoreColor.token(CoreColors.tertiary),
    minHeight: 12,
  ),
)

// 원형 진행률
RadialProgress(
  value: 75,
  showValueText: true,
)

// 원형 단계(size)와 의미(tone)
RadialProgress(
  value: 50,
  size: CoreProgressSize.md,
  tone: CoreProgressTone.success,
)
// 기본 진행률 바 (0.0 ~ 1.0 범위)
Progress(value: 0.65)

// 커스텀 min/max 범위 지정
Progress(value: 75, min: 0, max: 100)

// 채움이 무엇을 뜻하는지는 tone 으로 — 트랙은 모든 tone 에서 같은 중립색
Progress(value: 0.92, tone: CoreProgressTone.warning)

// 막대 위 라벨 · 값, 막대 아래 캡션 (모두 선택)
Progress(
  value: 0.62,
  label: Text('Team plan'),
  valueLabel: Text('62%'),
  caption: Text('AI usage · 18 days left'),
)

// 단계형 — 단계 수만큼 나뉜 막대. 지나온 단계는 채움 색, 남은 단계는 트랙 색
Progress(
  value: 1,
  max: 2,
  segments: 2,
  label: Text('STEP 1 / 2'),
)

// 색상/두께 커스터마이징은 progressStyle 슬롯으로
Progress(
  value: 0.5,
  progressStyle: CoreProgressStyle(
    color: CoreColor.token(CoreColors.tertiary),
    minHeight: 12,
  ),
)

// 원형 진행률
RadialProgress(
  value: 75,
  showValueText: true,
)

// 원형 단계(size)와 의미(tone)
RadialProgress(
  value: 50,
  size: CoreProgressSize.md,
  tone: CoreProgressTone.success,
)

빠른 오버라이드 (Chain)#

이미 만든 Progress 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius9999처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius9999 == CoreRadius.radius9999) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

색 getter(.primary · .success · .error …)는 채움(color)을 칠하고, getter 가 정한 색은 tone 보다 이깁니다. 채움이 무엇을 뜻하는지(한도에 가까움 · 실패) 말하려면 tone 을 쓰고, getter 는 tone 에 없는 색에 씁니다. 트랙(backgroundColor)은 비워 두므로 어떤 채움 아래서도 같은 중립 트랙(surfaceContainerHigh)입니다 — Progress(value: 0.5).success 는 success 채움 + 중립 트랙입니다. 트랙 색을 따로 정하려면 withStyle(CoreProgressStyle(backgroundColor: …)) 를 쓰고, 채움 색을 옅게 따라가는 트랙이 필요하면 backgroundAlpha 를 줍니다. .transparent 만은 트랙을 비웁니다 — 다른 컴포넌트에서처럼 '바탕 없음' 이라는 뜻이고, 채움을 투명하게 하면 막대가 보이지 않기 때문입니다.

class ProgressChainExample extends StatefulWidget {
  const ProgressChainExample({super.key});

  @override
  State<ProgressChainExample> createState() => _ProgressChainExampleState();
}

class _ProgressChainExampleState extends State<ProgressChainExample> {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        SizedBox(
          width: double.infinity,
          child: const Progress(value: 0.65).radius16.primary,
        ),
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(minHeight)까지 한 번에.
        SizedBox(
          width: double.infinity,
          child: const Progress(value: 0.65).withStyle(
            const CoreProgressStyle(
              color: CoreColor.token(CoreColors.tertiary),
              borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
              minHeight: CoreSpace.space16,
            ),
          ),
        ),
      ],
    );
  }
}
class ProgressChainExample extends StatefulComponent {
  const ProgressChainExample({super.key});

  @override
  State<ProgressChainExample> createState() => _ProgressChainExampleState();
}

class _ProgressChainExampleState extends State<ProgressChainExample> {
  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Progress(value: 0.65).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(minHeight)까지 한 번에.
        Progress(value: 0.65).withStyle(
          const CoreProgressStyle(
            color: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            minHeight: CoreSpace.space16,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Live Preview#

사용 시기 (When to Use)#

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

  • 파일 업로드, 다운로드 등 작업 진행률을 표시할 때
  • 목표 달성률, 스토리지 사용량 등 비율을 시각화할 때
  • 사용량이 한도에 가까운지 · 작업이 실패했는지처럼 채움 색으로 상태를 함께 말해야 할 때 (tone)

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

  • Loading: 단순 로딩 스피너가 필요할 때
  • Skeleton: 콘텐츠 로딩 플레이스홀더가 필요할 때
  • Slider: 사용자가 값을 조절해야 할 때

Props / Parameters#

모든 chrome / dimensional override는 단일 Style 슬롯 (progressStyle / radialProgressStyle) 한 곳으로 모입니다. 위젯 평면 파라미터는 behaviour (value / min / max / disableAnimation / segments / showValueText), 시맨틱 축 (size / tone), 합성 슬롯 (label / valueLabel / caption / child) 만 남습니다.

직선형의 label · valueLabel · caption 과 원형의 child 에 쓰는 W는 Flutter의 Widget, Web의 Component입니다. 직선형 값은 min–max 범위, 원형 값은 0–100 퍼센트 단위입니다.

Progress (직선형)#

속성타입기본값설명
value double? null 진행률 (0.0~1.0) — null → 무한 슬라이드
mindouble0.0최소값
maxdouble1.0최대값
disableAnimation bool false 결정형(determinate) 트랜지션 비활성화
segments int? null 막대를 이 수의 칸으로 나눕니다 — 지나온 단계는 채움 색, 남은 칸은 트랙 색, 칸 사이는 segmentSpacing . null → 연속 막대. 불확정( value: null )이면 무시합니다. 1 이상
size CoreProgressSize defaultSize (sm) 직선형 진행바의 크기 토큰
tone CoreProgressTone defaultTone (primary) 채움 색이 뜻하는 것 — primary · success · warning · destructive . 트랙은 모든 tone 에서 같은 중립색
label W? null 막대 위 왼쪽 칸 — 막대의 이름
valueLabel W? null 막대 위 오른쪽 칸 — 값 (예: 62%)
caption W? null 막대 아래 칸 — 보조 설명
progressStyle CoreProgressStyle? null chrome 슬롯 (아래 표)

CoreProgressStyle 필드#

필드타입설명
colorCoreColor?Progress fill colour override.
backgroundColor CoreColor? Track background colour override. null (default) → [defaultBackgroundColor], unless [backgroundAlpha] is stated.
backgroundAlpha double? Track background alpha — a track derived from the fill ( color faded by this alpha) instead of the neutral [defaultBackgroundColor]. Applied only when [backgroundColor] is not set. null (default) → the neutral track.
borderRadius CoreBorderRadius? Border radius override. Applies to both track and indicator so rounded shapes remain visually consistent.
minHeight double? Minimum height (thickness) override (logical px). null defers to [defaultsBySize] for the active CoreProgressSize .
duration Duration? Determinate width transition duration override.
indeterminateDuration Duration? Indeterminate slide cycle duration override.
indeterminateFillRatio double? Indeterminate fill width as a share of the track, 0..1 . null defers to [defaultIndeterminateFillRatio]. Both platforms derive the slide's travel from it.
labelStyle CoreTextStyle? label cell text override, merged field by field onto [defaultLabelStyle]. Applied as the ambient text style of whatever the caller puts in the label slot.
valueLabelStyle CoreTextStyle? valueLabel cell text override, merged field by field onto [defaultValueLabelStyle].
captionStyle CoreTextStyle? caption cell text override, merged field by field onto [defaultCaptionStyle].
labelGapStyle CoreGapStyle? Spacer between the label row and the bar — drawn only when the bar has a label or valueLabel . Merged onto [defaultLabelGapStyle].
captionGapStyle CoreGapStyle? Spacer between the bar and the caption — drawn only when the bar has a caption . Merged onto [defaultCaptionGapStyle].
segmentSpacing double? Gap between the cells of a segmented bar ( Progress.segments ), in logical pixels — Row.spacing (Flutter) / the cell row's column-gap (Web). A continuous bar never reads it. null defers to [defaultSegmentSpacing].

RadialProgress (원형)#

속성타입기본값설명
valuedouble필수진행률 (0~100)
showValueText bool false 퍼센트 텍스트 표시 (sm 은 링이 작아 그리지 않음)
size CoreProgressSize lg 크기 단계 — sm 32 · md 48 · lg 80 · xl 128 (링 두께 3 · 4 · 6 · 8)
tone CoreProgressTone primary 아크 색의 의미 — primary · success · warning · destructive
childW?null중앙 커스텀 콘텐츠
radialProgressStyle CoreRadialProgressStyle? null chrome 슬롯 (아래 표)

CoreRadialProgressStyle 필드 (chrome)

필드타입기본값설명
color CoreColor? CoreColors.primary 아크 색상
backgroundColor CoreColor? CoreColors.surfaceContainerHigh (직선형 트랙과 같은 중립색) 트랙 링 색상
backgroundAlpha double? null → 중립 트랙 값을 주면 backgroundColor 가 없을 때 아크 색을 이 알파로 페이드한 트랙 링
size double? CoreSize.size80 (80px, lg) 원 지름. defaultsBySize 가 단계마다 따로 정합니다
strokeWidth double? CoreStrokeWidth.stroke6 (lg) 링 두께. 단계마다 defaultsBySize 가 정합니다
valueTextStyle CoreTextStyle? 단계가 정한 역할( md labelSmall · lg titleMedium · xl headlineLarge — 좁은 화면에서는 글자 단계가 줄입니다) + onSurface 중앙 퍼센트 텍스트. sm 은 값이 없어 그리지 않습니다

동작 스펙 (Behavior)#

직선형#

  • value 변경 시 부드러운 width 애니메이션 (300ms ease-in-out)
  • 인디케이터에 borderRadius 적용 — 양쪽 둥근 모양
  • 트랙 배경 기본값: surfaceContainerHigh (채움 색을 따라가지 않는 중립 트랙)
  • 채움 색은 tone 이 정합니다 — primary · success · warning · destructive 가 각각 primary · success · warning · error 를 칠합니다. 불확정(value: null) 막대도 같은 tone 색입니다.
  • 라벨 칸: label(왼쪽, 남은 폭을 채움)과 valueLabel(오른쪽)이 한 행으로 막대 위에, caption 이 막대 아래에 놓입니다. 칸과 막대 사이는 labelGapStyle · captionGapStyle(기본 8). label 없이 valueLabel 만 주면 값은 오른쪽에 그대로 있습니다.
  • 칸의 글자: label labelMedium · 600 · onSurface, valueLabel labelMedium · onSurfaceVariant, caption bodyXs · onSurfaceSubtle. 칸에 넣은 위젯이 이 스타일을 이어받고, 다르게 하려면 Text 체인으로 덮습니다.
  • 세 칸이 모두 null 이면 막대 하나만 그립니다 — 칸이 생기기 전과 같은 트리 · DOM 입니다.
  • segments 를 주면 막대가 그 수의 칸으로 나뉩니다. 채워지는 칸은 지나온 단계 수 — ⌊(value − min) / (max − min) × segments⌋ — 이고 나머지 칸은 트랙 색입니다. 단계는 도달해야 지나간 것으로 셉니다(value: 0.99, segments: 2 는 한 칸). 곱셈의 부동소수 오차로 정수 바로 아래에 떨어진 값(0.29 × 100 = 28.999…)은 그 단계에 도달한 것으로 봅니다. 두 플랫폼이 같은 계산(CoreProgressContract.filledSegmentsOf)을 부릅니다.
  • 칸은 폭을 똑같이 나누고(Flutter Expanded, Web flex-1) 높이 · 모서리는 트랙과 같습니다. 칸 사이는 segmentSpacing(기본 space4) 입니다. 기본값이 밀도 축의 바닥(4 이하는 움직이지 않음)이라 밀도를 바꿔도 같고, 더 큰 값을 직접 주면 밀도를 따라 움직입니다. 칸 색이 바뀔 때는 duration 동안 CoreEasing.move 로 전환합니다 — 연속 막대의 채움 폭과 같은 시간 · 곡선입니다.
  • 불확정 막대에는 보여 줄 단계가 없어 segments 를 주어도 연속 막대로 미끄러집니다.

원형#

  • 12시 방향(-π/2)에서 시작하는 원형 아크
  • strokeCap: round — 양쪽 끝 둥근 형태
  • showValueText: true 시 중앙에 "75%" 형태 표시
  • 직선형과 같은 size(4단 — xs2 · xs 는 없음) · tone 축을 가집니다. 단계가 지름 · 링 두께 · 글자 역할을 함께 정하고, tone 이 아크 색을 정합니다. 트랙은 어느 tone 에서나 중립 면(surfaceContainerHigh) 입니다.
  • label · valueLabel · caption 이 없습니다 — 중앙에는 child 한 칸만 있습니다. 직선형과 API 가 갈리는 자리입니다.
  • 값 칸(valueLabel)은 가장 넓은 읽기 100% 의 너비를 미리 잡고 숫자를 끝에 붙입니다 — 9% → 10% → 100% 로 올라가도 막대가 밀리지 않습니다 (숫자는 같은 폭의 자릿수).
  • 불확정 막대의 움직이는 채움은 트랙의 35% 입니다 (indeterminateFillRatio).

사용 가이드라인 (Usage Guidelines)#

완료 비율이 알려진 작업은 Progress/RadialProgress로 표시하고 알려지지 않은 작업은 Progress의 value: null 또는 Loading을 사용합니다. 기다리는 콘텐츠의 모양을 예약하려면 호출자가 Skeleton 트리를 구성합니다. 값을 사용자가 조절해야 하면 Slider를 선택합니다.

두 끝 값을 조절하는 구간 입력은 Range, 서로 다른 시점의 상태 구간은 Tracker이며 단일 완료 비율과 구분합니다.

Do#

원형 진행률은 대시보드 통계에 적합합니다.

RadialProgress(
  value: 75,
  showValueText: true,
)

공간이 제한된 카드에서 목표 달성률을 효과적으로 표시합니다.

Don't#

진행률이 멈춘 상태에서 아무 피드백 없이 두지 마세요.

진행이 느리면 "대용량 파일 처리 중..." 같은 텍스트를 추가하세요.

접근성 (Accessibility)#

역할#

WebFlutter
Progress role="progressbar" + aria-valuemin / aria-valuemax , 값이 있을 때만 aria-valuenow . label 이 있으면 aria-labelledby , caption 이 있으면 aria-describedby . 단계형이면 aria-valuetext Semantics(value: ...) — progressbar 역할도 min/max 도 없음. 칸이 있으면 MergeSemantics 로 한 노드가 되어 label · caption 글자가 이름으로 읽힘
RadialProgress role="progressbar" + aria-valuenow (min/max 없음 → ARIA 기본 0–100) 없음 — 파일 어디에도 Semantics 가 없음
  • Web 의 role / aria-value* 는 caller attributes 보다 나중에 병합되어 덮어쓸 수 없습니다. 그 밖의 속성(aria-label 등)은 그대로 통과합니다.
  • Progress 는 label 을 주면 그 글자가 막대의 이름이 됩니다(Web aria-labelledby, Flutter 병합된 노드의 라벨). caption 은 Web 에서 aria-describedby 로 이름과 갈라진 설명이 되고, Flutter 에서는 노드가 하나로 합쳐져 이름(라벨)에 이어 읽힙니다 — 리더가 읽는 글자는 같지만 이름과 설명을 따로 구별할 수 있는 것은 Web 뿐입니다. valueLabel 은 값을 화면에 다시 쓰는 칸이라 두 플랫폼 모두 읽지 않습니다 — 값은 막대 자신의 값으로 한 번만 읽힙니다.
  • label 이 없는 Progress 와 RadialProgress 는 이름이 없습니다 — 무엇의 진행인지 알려주는 라벨을 붙이지 않습니다.

키보드#

해당 없음. 진행률 표시는 포커스를 받지 않고 tab stop 을 만들지 않습니다. 표시 전용 이라 값을 바꾸는 키도 없습니다 — 사용자가 값을 조절해야 하면 Slider 를 쓰세요.

스크린 리더#

  • Web — aria-valuenow 에는 정규화하지 않은 원래 값이 실리고(Progress(value: 0.65) → aria-valuenow="0.65"), aria-valuemin / aria-valuemax 가 함께 있어 리더가 백분율을 계산합니다. value 가 null 인 indeterminate 상태에서는 aria-valuenow 를 아예 내보내지 않습니다.
  • Flutter — 값은 min~max 를 0–100 으로 정규화한 백분율 문자열('65%')로 실립니다. indeterminate 일 때는 숫자 대신 로케일의 로딩 문구를 값으로 내보냅니다 — 0% 를 읽어 "멈춘 것처럼" 들리는 것을 피하기 위해서입니다.
  • 단계형(segments)은 두 플랫폼 모두 백분율 대신 단계를 읽습니다 — 활성 로케일의 문구로 "Step 1 of 2" / "2단계 중 1단계". Web 은 이 문구를 aria-valuetext 에 싣고 aria-valuenow · aria-valuemin · aria-valuemax 는 원래 값 그대로 둡니다. Flutter 는 같은 문구를 Semantics 의 value 로 냅니다.
  • 값이 바뀔 때 통보하는 live region 은 어느 쪽에도 없습니다. 진행 상황을 소리로 알려야 하면 호출자가 별도 라이브 리전을 둬야 합니다.
  • RadialProgress 의 showValueText 가 그리는 NN% 텍스트는 Flutter 에서 이 컴포넌트가 읽히게 하는 유일한 경로입니다.

포커스 관리#

  • 진입 / 이탈 / 트랩 — 전부 없음. 오버레이도 컨트롤도 아니라 포커스를 옮기지 않습니다.

알려진 제약#

  • Flutter 에는 progressbar 역할이 없습니다. CoreSemanticRole.progressBar 는 존재 하지만 Progress 는 raw Semantics(value:) 만 쓰고 role · minValue · maxValue 를 넘기지 않습니다. 리더는 "진행률 표시줄"이라고 안내하지 않고 값만 읽습니다. 대안: 호출부가 값·최소·최대를 다시 적어 Semantics(role: ..., value:, minValue:, maxValue:) 로 감쌉니다.
  • RadialProgress 는 Flutter 스크린 리더에 존재하지 않습니다 — 시맨틱 배선이 전혀 없습니다. 대안: 호출부에서 Semantics(...) 로 감쌉니다. Web 은 역할이 있으나 aria-valuemin / aria-valuemax 가 없어 ARIA 기본값 0–100 에 의존합니다 — value 가 0–100 스케일이라 결과는 맞지만 범위를 바꿀 진입점이 없습니다.
  • 화면에 라벨을 그리지 않고 이름만 붙이는 진입점은 플랫폼마다 다릅니다. 보이는 라벨이면 두 플랫폼 모두 label 슬롯이 이름이 됩니다. 보이지 않는 이름만 필요하면 Web 은 Progress(attributes: {'aria-label': ...}) 로 통과시킬 수 있고(강제되는 것은 role 과 aria-value*, 칸이 있을 때의 aria-labelledby · aria-describedby), Flutter 는 호출부가 Semantics(label: ...) 로 감싸야 합니다.
  • OS 색 강제 모드에서 진행 위치가 사라질 수 있습니다. 트랙과 채움이 보더 없이 배경색만으로 구분되므로 두 색이 같은 시스템 색으로 치환되면 구분이 없어집니다. Web 은 그때도 role="progressbar" / aria-valuenow 로 값 자체는 계속 전달합니다.

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

항목FlutterWeb
클래스명 Progress / RadialProgress Progress / RadialProgress
값 범위0.0~1.0 (기본)0.0~1.0 (기본)
직선형 렌더링 Container + AnimatedContainer div + inline style
라벨 칸 배치 칸이 있으면 Column[Row(label, valueLabel), Gap, 트랙, Gap, caption] 칸이 있으면 flex flex-col 루트 아래 같은 순서 — 루트가 role="progressbar" 를 유지
단계형 렌더링 트랙 자리에 Row(spacing: segmentSpacing) + Expanded 칸, 칸 색 전환은 AnimatedContainer 트랙 자리에 flex 행 + column-gap + flex-1 칸, 칸 색 전환은 CSS background-color transition — 라벨 칸이 없으면 이 행이 role="progressbar" 루트
원형 렌더링 CustomPaint (_RadialProgressPainter) inline <svg> + <circle> 2개 (트랙 + stroke-dasharray / stroke-dashoffset 아크)
애니메이션AnimatedContainer 300msCSS transition 300ms
  • Loading: 로딩 스피너
  • Skeleton: 콘텐츠 플레이스홀더
  • Slider: 값 조절 (Progress는 읽기 전용)