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

OverflowMarquee

콘텐츠가 컨테이너를 넘칠 때 자동으로 스크롤시키는 마르키 컴포넌트

목차

OverflowMarquee는 자식 콘텐츠가 컨테이너를 넘칠 때 좌우(또는 상하)로 자동 스크롤시키는 마르키 레이아웃입니다. 곡 제목, 뉴스 티커, 긴 상태 라벨 등에 사용합니다.

기본 사용법#

SizedBox(
  width: CoreSpace.space240,
  child: OverflowMarquee(
    pauseOnHover: true,
    child: Text('A long running headline that overflows the box.'),
  ),
)
div(
  styles: Styles(raw: {'width': '${CoreSpace.space240}px'}),
  [
    OverflowMarquee(
      pauseOnHover: true,
      child: const Text('A long running headline that overflows the box.')
          .bodyMedium
          .onSurface,
    ),
  ],
)

빠른 오버라이드 (Chain)#

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

class OverflowMarqueeChainExample extends StatelessWidget {
  /// Creates an [OverflowMarqueeChainExample].
  const OverflowMarqueeChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space240,
      child:
          OverflowMarquee(
            pauseOnHover: true,
            child: Text(
              'A long running headline that overflows the box and scrolls.',
            ).bodyMedium.onSurface,
          ).withStyle(
            const CoreOverflowMarqueeStyle(
              duration: Duration(milliseconds: CoreDuration.ms5000),
              delayDuration: Duration(milliseconds: CoreDuration.slow),
              fadePortion: CoreSpace.space48,
            ),
          ),
    );
  }
}
class OverflowMarqueeChainExample extends StatelessComponent {
  /// Creates an [OverflowMarqueeChainExample].
  const OverflowMarqueeChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      styles: Styles(raw: {'width': '${CoreSpace.space240}px'}),
      [
        OverflowMarquee(
          pauseOnHover: true,
          child: const Text(
            'A long running headline that overflows the box and scrolls.',
          ).bodyMedium.onSurface,
        ).withStyle(
          const CoreOverflowMarqueeStyle(
            duration: Duration(milliseconds: CoreDuration.ms5000),
            delayDuration: Duration(milliseconds: CoreDuration.slow),
            fadePortion: CoreSpace.space48,
          ),
        ),
      ],
    );
  }
}

Live Preview#

사용 시기#

  • 제한된 공간에 긴 텍스트를 끊기지 않게 보여주고 싶을 때
  • 뉴스 헤드라인이나 상태 메시지를 한 줄에 흘리는 UI
  • 탭/카드 라벨의 truncation 대신 전체 텍스트를 회전시키고 싶을 때

Props / Parameters#

W는 Flutter의 Widget, Web의 Component를 뜻합니다.

속성타입기본값설명
childW필수스크롤 대상 콘텐츠
direction CoreAxis horizontal 스크롤 축
curve CoreMarqueeCurve? linear 이징 (linear/easeIn/easeOut/easeInOut)
pauseOnHover bool false 마우스 hover 시 일시정지
overflowMarqueeStyle CoreOverflowMarqueeStyle? null chrome 슬롯 (아래 표)

스타일 시스템 (Style System)#

CoreOverflowMarqueeStyle 필드#

필드타입설명
duration Duration? Time for one full scroll cycle override.
delayDuration Duration? Pause between cycles override.
step double? Pixel step used to scale speed with content length.
fadePortion double? Length (px) of the edge fade gradient. Zero disables the fade.

동작 스펙 (Behavior)#

  • 양쪽 모두 콘텐츠와 뷰포트의 크기 차이를 측정합니다. 넘치지 않으면 정지하고, 넘치면 그 차이만큼 이동한 뒤 양 끝에서 대기하며 왕복합니다. 자식을 복제하는 무한 루프가 아닙니다.
  • direction은 이동 축, curve는 이동 이징을 선택합니다. 이동 시간은 duration × (넘친 길이 / step)이며 step과 fadePortion은 논리 px 단위, duration과 delayDuration은 Duration입니다.
  • pauseOnHover가 켜져 있으면 포인터 진입 중 경과 시간을 멈추고 이탈 시 같은 위치에서 이어갑니다. 자체 키보드 정지 컨트롤이나 값 변경 콜백은 없습니다.

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

✅ Do#

Flutter에서는 자식을 폭이 제한된 컨테이너로 감싸기

SizedBox(
  width: CoreSpace.space240,
  child: OverflowMarquee(
    pauseOnHover: true,
    child: Text('A long running headline that overflows the box.').bodyMedium.onSurface,
  ),
)

양쪽 모두 자식이 실제로 이 박스를 넘칠 때만 스크롤합니다. 폭 제약이 없는 자리에 그대로 두면 넘칠 일이 없어 애니메이션이 시작되지 않습니다. 사용자가 직접 이동해야 한다면 자동 왕복 대신 ScrollableClient를 선택합니다.


❌ Don't#

pauseOnHover만으로 키보드 사용자가 멈출 수 있다고 가정하지 않기

// ❌ 키보드 사용자가 멈출 방법이 pauseOnHover 하나뿐
OverflowMarquee(
  pauseOnHover: true,
  child: stockTickerText,
)

pauseOnHover는 양 플랫폼 모두 포인터 전용(Flutter MouseRegion, Web mouseenter/mouseleave)이라 포커스로는 멈추지 않습니다. 키보드만 쓰는 사용자가 읽을 시간이 필요한 콘텐츠라면 별도의 정지 컨트롤을 함께 제공해야 합니다.

접근성 (Accessibility)#

역할#

양 플랫폼 모두 아무 역할도 내보내지 않습니다. Flutter는 ClipRect + 레이아웃 위젯만 반환하고 Semantics 호출이 없으며, Web은 루트 <div> · <style> · 트랙 <div>에 호출자가 넘긴 attributes만 통과시킵니다 — role도, aria-live도, aria-label도, aria-hidden도 없습니다.

즉 이 컴포넌트는 자식의 시맨틱을 그대로 통과시키는 투명한 스크롤 상자입니다. 이 영역에 이름이나 역할이 필요하면 호출자가 직접 붙여야 합니다.

키보드#

처리하는 키가 없습니다. 스크롤을 멈추거나, 다시 시작하거나, 한 칸씩 넘기는 키보드 경로가 양 플랫폼 모두 없습니다.

포커스 관리#

포커스를 받지 않습니다. Flutter에 FocusNode/Focus가 없고 Web 루트에 tabindex가 없으며, 포커스가 들어와도 스크롤이 반응하지 않습니다. 다만 Flutter는 applyPaintTransform/hitTestChildren을 스크롤 오프셋에 맞춰 재정의하므로, 자식이 그려진 위치 그대로 히트 테스트됩니다 — 자식이 원래 갖고 있던 클릭 가능성은 어긋나지 않습니다(포커스 가능해지는 것은 아닙니다).

스크린 리더#

읽히는 것은 자식의 내용뿐입니다. Flutter는 페인트 오프셋이 시맨틱 사각형에 그대로 반영되므로 리더가 보고하는 위치와 화면에 그려진 위치가 일치합니다. 어느 플랫폼도 "지금 움직이는 중", "내용이 잘렸음", "일부가 화면 밖에 있음"을 알리지 않습니다.

알려진 제약#

  • pauseOnHover는 포인터 전용입니다. Flutter는 MouseRegion, Web은 컨테이너의 mouseenter/mouseleave로만 일시정지하며, 포커스로 멈추는 경로도 별도의 정지 컨트롤도 없습니다 — 키보드만 쓰는 사용자는 움직임을 멈추고 읽을 방법이 없습니다.
  • 동작 줄이기(reduced motion)가 Web에는 닿지 않습니다. Flutter는 순회/대기 duration을 resolveMotionDuration으로 통과시키지만, Web은 duration을 CSS 변수(--coui-duration-*)가 아닌 평범한 Dart 값으로 rAF 측정기에 넘기고 그 측정기에는 prefers-reduced-motion 검사가 없습니다. 따라서 전역 접근성 축의 reduced-motion 오버라이드가 Flutter에서만 적용됩니다.
  • 역할·이름·live region이 전혀 없으므로, 마퀴 안의 내용이 갱신되는 정보(주식 시세·알림 등)라면 알림 자체는 호출자가 별도로 마련해야 합니다.

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

항목FlutterWeb
크기 측정 render object의 layout DOM getBoundingClientRect와 ResizeObserver
왕복 이동 Ticker와 페인트 오프셋 requestAnimationFrame과 CSS transform
가장자리 페이드진행률 기반 shader mask진행률 기반 CSS mask 변수
실행 환경마운트된 Flutter 트리에서 동작브라우저에 마운트된 뒤 동작; SSR은 정적 콘텐츠
모션 줄이기 resolver의 resolveMotionDuration 경유 현재 rAF 측정기는 prefers-reduced-motion을 조회하지 않음

관련 컴포넌트#