OverflowMarquee | CoUI
LogoCoUI

OverflowMarquee

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

OverflowMarquee#

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

Live Preview#

사용 시기#

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

기본 사용법#

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,
          ),
        ),
      ],
    );
  }
}

Props#

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

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.

동작 차이#

  • Flutter: 자식이 실제로 넘칠 때만 스크롤. 끝에서 되돌아오는 왕복 애니메이션.
  • Web: 자식을 복제해 무한 루프로 스크롤 (@keyframes translateX). 넘치지 않더라도 루프가 계속됨.

어느 플랫폼이든 pauseOnHover/direction/curve 위젯 파라미터와 overflowMarqueeStyleduration/delayDuration/step/fadePortion은 동일하게 적용됩니다.

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

✅ Do#

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

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

Flutter는 자식이 실제로 이 박스를 넘칠 때만 스크롤합니다. 폭 제약이 없는 자리에 그대로 두면 넘칠 일이 없어 애니메이션이 아예 시작되지 않습니다 — Web은 반대로 넘치지 않아도 항상 무한 루프이므로, 폭을 제한해야 두 플랫폼이 같은 그림을 보여줍니다.


❌ Don't#

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

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

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

접근성 (Accessibility)#

역할 / Semantics#

양 플랫폼 모두 아무 역할도 내보내지 않습니다. 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이 전혀 없으므로, 마퀴 안의 내용이 갱신되는 정보(주식 시세·알림 등)라면 알림 자체는 호출자가 별도로 마련해야 합니다.

관련 컴포넌트#