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

Countdown

숫자 값을 표시하는 카운트다운 디스플레이 컴포넌트

목차

숫자 값(0-99)을 표시하는 디스플레이 컴포넌트입니다. 값이 변경될 때 전환 애니메이션이 적용됩니다. 타이머 로직은 부모 컴포넌트에서 관리합니다.

기본 사용법 (Basic Usage)#

// 기본 카운트다운
Countdown(value: 42)

// 크기 조절
Countdown(
  value: 42,
  countdownStyle: CoreCountdownStyle(fontSize: 64),
)

// 시:분:초 조합
Row(
  children: [
    Countdown(value: hours),
    Text(':'),
    Countdown(value: minutes),
    Text(':'),
    Countdown(value: seconds),
  ],
)
// 기본 카운트다운
Countdown(value: 42)

// 크기 조절
Countdown(
  value: 42,
  countdownStyle: CoreCountdownStyle(fontSize: 64),
)

// 시:분:초 조합
div([
  Countdown(value: hours),
  Text(':'),
  Countdown(value: minutes),
  Text(':'),
  Countdown(value: seconds),
], classes: 'flex items-center gap-${CoreSpace.scale.space4}')

빠른 오버라이드 (Chain)#

이미 만든 Countdown 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

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

  @override
  State<CountdownChainExample> createState() => _CountdownChainExampleState();
}

class _CountdownChainExampleState extends State<CountdownChainExample> {
  @override
  Widget build(BuildContext context) {
    return const Countdown(value: 42).withStyle(
      const CoreCountdownStyle(
        fontSize: CoreFontSize.s48,
        color: CoreColor.token(CoreColors.primary),
        animationDuration: Duration(milliseconds: CoreDuration.moderate),
      ),
    );
  }
}
class CountdownChainExample extends StatefulComponent {
  const CountdownChainExample({super.key});

  @override
  State<CountdownChainExample> createState() => _CountdownChainExampleState();
}

class _CountdownChainExampleState extends State<CountdownChainExample> {
  @override
  Component build(BuildContext context) {
    return Countdown(value: 42).withStyle(
      const CoreCountdownStyle(
        fontSize: CoreFontSize.s48,
        color: CoreColor.token(CoreColors.primary),
        animationDuration: Duration(milliseconds: CoreDuration.moderate),
      ),
    );
  }
}

Live Preview#

사용 시기 (When to Use)#

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

  • 카운트다운 숫자를 크게 표시할 때
  • 시간 단위별 숫자를 개별 표시할 때 (시:분:초 각각)
  • 세션 타임아웃 등 남은 시간을 숫자로 표시할 때

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

  • NumberTicker: 클램핑한 정수 교체 대신 숫자 값 사이를 보간해야 할 때
  • Progress: 숫자 표시 대신 진행값을 비율로 시각화할 때

Props / Parameters#

속성타입기본값설명
value int 필수 표시할 숫자 값 (min~max 로 클램핑)
minint0클램핑 하한
maxint99클램핑 상한
countdownStyle CoreCountdownStyle? null 폰트/색상/애니메이션 등 chrome override

스타일 시스템 (Style System)#

모든 chrome / 애니메이션 override 는 countdownStyle 슬롯 하나로 흐릅니다.

CoreCountdownStyle 필드#

필드타입설명
fontSize double? Display font size override (logical px). No default* , and it must not get one. Both resolvers fold this flat field into an overlay that merges ON TOP of [defaultLabelStyle] ⊕ [labelStyle], so whatever sits here outranks both — and the resting font size is already stated, by the headlineLarge role inside [defaultLabelStyle]. A static const would therefore (a) make the [labelStyle] slot's own fontSize unreachable and (b) replace a token role with a raw scalar, which on Flutter also drops out of the text-scaling axis the role is materialised through. The resolvers rely on the overlay being null-only to stay a no-op; on Web a non-null one turns emitTypography from "role class, no inline" into a permanent inline font-size that beats the class.
color CoreColor? Display text colour override. No default* — same overlay position as [fontSize], and the resting colour is likewise already stated: onSurface , inside [defaultLabelStyle]. A default here would win over labelStyle.color , inverting the documented precedence, and would emit a standing inline color on Web where only the token class renders today.
animationDuration Duration? Digit transition / fade animation duration override. NOT the total countdown length — that is a semantic widget parameter (see contract value ).
labelStyle CoreTextStyle? Label text style override (applied to the numeric child).

fontSize / color 는 labelStyle 슬롯 위에 덧씌워지므로 flat 필드가 이깁니다.

Resolve chain#

CoreCountdownStyle.defaultX (static const, 단일 출처)
  → CoreCountdownTheme.style                      // 프로젝트 공통
  → widget.countdownStyle                         // 인스턴스별

동작 스펙 (Behavior)#

인터랙션#

  • Countdown은 표시 전용 컴포넌트입니다. 인터랙션 없음.
  • 부모 컴포넌트에서 Timer로 value를 업데이트해야 합니다.
  • value.clamp(min, max) 의 정수만 표시하며 자체 타이머·완료 콜백·단위·비활성 상태는 없습니다. 숫자가 초인지 분인지는 호출부가 정합니다. min <= max 를 보장하세요.

애니메이션#

  • Flutter: AnimatedSwitcher + FadeTransition으로 값 변경 시 페이드 전환
  • Web: CSS transition chrome은 있지만 텍스트 변경을 두 노드 간 페이드로 교체하는 상태는 없습니다
  • 양쪽 모두 countdownStyle.animationDuration(기본 CoreDuration.fast 150ms)을 씁니다

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

✅ Do#

시:분:초처럼 자릿수 단위로 인스턴스를 나눠서 표시

Row(
  children: [
    Countdown(value: hours),
    Text(':'),
    Countdown(value: minutes),
    Text(':'),
    Countdown(value: seconds),
  ],
)

Countdown은 min(기본 0)~max(기본 99) 범위의 두 자릿수 값 하나만 표시하도록 설계되어 있습니다 — 더 큰 시간 단위를 표현하려면 단위별로 인스턴스를 나눠야 합니다.


❌ Don't#

Countdown 내부에 타이머 로직이 있다고 기대하지 않기

// ❌ value를 한 번만 넘기고 자동으로 줄어들 거라 기대
Countdown(value: 60)

Countdown은 표시 전용 컴포넌트로 인터랙션도 내부 타이머도 없습니다 — 부모 컴포넌트가 Timer로 value를 직접 갱신해야 카운트다운이 동작합니다.

접근성 (Accessibility)#

Countdown 은 표시 전용입니다 — 포커스를 받지 않고, 값 갱신은 부모가 합니다. 접근성 표면은 "바뀐 숫자를 읽어주는 것" 하나뿐입니다.

역할#

  • Flutter — Semantics(liveRegion: true, label: <클램핑된 값>). 버튼·이미지 같은 역할 플래그는 세우지 않고 live region 만 겁니다.
  • Web — <span> 에 aria-live="polite" + aria-label="<클램핑된 값>". role 은 emit 하지 않습니다.

키보드#

없음. 양 플랫폼 모두 탭 순서에 들어가지 않고(Web 루트에 tabindex 가 붙지 않습니다) 처리하는 키도 없습니다.

스크린 리더#

  • 이름 — value.clamp(min, max) 결과 문자열 그 자체입니다. Flutter 는 Semantics(label:), Web 은 aria-label 에 같은 값을 넣으므로 리더가 읽는 숫자는 화면의 숫자와 항상 같습니다.
  • 통보 — 양 플랫폼 모두 live region 입니다. 값이 바뀔 때마다 polite 로 읽힙니다 (Flutter liveRegion: true, Web aria-live="polite").

포커스 관리#

없음. 오버레이가 아니고 포커스 가능 요소도 아니라 진입·이탈·트랩이 모두 해당되지 않습니다.

알려진 제약#

  • 읽히는 것은 숫자뿐입니다. 단위나 대상("42초 남음")을 넣을 파라미터가 없습니다 — contract 는 value / min / max / countdownStyle 만 노출합니다. 대안: 맥락은 인접한 텍스트로 제공합니다.
  • live region 을 끄는 파라미터가 없습니다. 부모가 1초마다 value 를 갱신하면 리더도 1초마다 통보합니다. 대안: 갱신 주기를 부모에서 조절합니다.
  • Web 은 aria-live / aria-label 을 caller attributes 보다 나중에 병합합니다. 대부분의 Web 컴포넌트는 caller 의 attributes 가 이기지만 이 두 키만은 컴포넌트가 이겨서, attributes: {'aria-label': '...'} 는 무시됩니다. 대안 없음.
  • 토큰이 아닌 animationDuration 은 Web 에서 '동작 줄이기'를 빠져나갑니다. 기본값(CoreDuration.fast)이나 스케일 위 값은 var(--coui-duration-*) 로 나가 prefers-reduced-motion 이 0 으로 덮지만, 스케일 밖 값(예 Duration(milliseconds: 175))은 리터럴 175ms 로 나가 그대로 재생됩니다. Flutter 는 값과 무관하게 resolveMotionDuration 을 거칩니다. 대안: CoreDuration 의 alias(fast / normal / moderate)나 msN 스케일 값을 씁니다.

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

항목FlutterWeb
클래스명CountdownCountdown
값 변경 값 key가 다른 텍스트의 AnimatedSwitcher fade 같은 span의 텍스트 교체; 값별 cross-fade는 현재 결손
숫자 정렬FontFeature.tabularFigures()tabular-nums
  • NumberTicker: 일반 숫자 카운터 애니메이션
  • Stat: 레이블 + 값 형식의 통계 표시