FadeScroll | CoUI
LogoCoUI

FadeScroll

스크롤 영역 가장자리에 페이드 효과를 적용하는 wrapper 컴포넌트

FadeScroll#

FadeScroll 은 스크롤 가능한 [child] 를 감싸고 그 가장자리에 그라디언트 페이드 마스크를 씌워 "더 많은 콘텐츠가 보이지 않는 영역에 있음" 을 시각적으로 암시합니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 가로 스크롤 카드 리스트의 좌/우 끝에 콘텐츠가 부드럽게 사라지게 표현할 때
  • 세로 스크롤 컨테이너 위/아래에 페이드 그라디언트로 추가 콘텐츠 존재를 암시할 때
  • 모달/드로어 내부의 긴 콘텐츠가 보더 영역까지 닿을 때 부드러운 시각 전환

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

  • OverflowMarquee: 텍스트가 컨테이너 너비를 넘을 때 자동 스크롤되는 효과가 필요할 때
  • ScrollableClient: 양 끝 그라디언트가 필요 없는 단순 스크롤 영역

기본 사용법 (Basic Usage)#

// 기본 — 세로 스크롤 + 양 끝 페이드 (40px default)
FadeScroll(
  child: Column(children: items),
)

// 가로 스크롤 + style 슬롯으로 페이드 영역 32px 지정
FadeScroll(
  direction: CoreFadeScrollDirection.horizontal,
  fadeScrollStyle: CoreFadeScrollStyle(
    startOffset: CoreSpace.space32,
    endOffset: CoreSpace.space32,
  ),
  child: Row(children: cards),
)
// 기본 — 세로 스크롤 + 양 끝 페이드 (40px default)
FadeScroll(
  child: div([for (final item in items) ...]),
)

// 가로 스크롤 + style 슬롯으로 페이드 영역 32px 지정
FadeScroll(
  direction: CoreFadeScrollDirection.horizontal,
  fadeScrollStyle: CoreFadeScrollStyle(
    startOffset: CoreSpace.space32,
    endOffset: CoreSpace.space32,
  ),
  child: div(
    [for (final c in cards) ...],
    styles: Styles(raw: {'display': 'flex'}),
  ),
)

Props / Parameters#

속성타입기본값설명
child Widget (Flutter) / Component (Web) 필수 스크롤 가능한 콘텐츠
direction CoreFadeScrollDirection CoreFadeScrollDirection.vertical 스크롤 축 (vertical / horizontal)
controller ScrollController? null (auto-create) — Flutter 전용 스크롤 컨트롤러. Web 은 브라우저 native scroll 이 같은 capability 를 제공
showScrollbar bool false 네이티브 스크롤바 노출 여부
fadeScrollStyle CoreFadeScrollStyle? null per-instance chrome / dimensional override ( startOffset , endOffset , gradient )

CoreFadeScrollStyle 필드#

필드타입설명
startOffset double? Pixel offset from the start edge over which the fade ramps up.
endOffset double? Pixel offset from the end edge over which the fade ramps down.
gradient List<CoreColor>? Optional gradient color stops applied to the fade. The list is interpolated from the visible edge inward.
gradienta =

동작 스펙 (Behavior)#

  • Flutter: ListenableBuildercontroller 위치를 listen → 스크롤 끝 도달 시 그쪽 페이드 자동 제거 (dynamic).
  • Web: mask-image + JS FadeScrollMeasureronScroll 마다 CSS variable (--co-fade-start-alpha, --co-fade-end-alpha) 을 업데이트해 Flutter 와 동일한 dynamic 동작.

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

✅ Do#

스크롤 가능 여부가 중요하면 showScrollbar: true를 켜기

FadeScroll(
  showScrollbar: true,
  child: Column(children: items),
)

기본값 showScrollbar: false는 넘침을 페이드 그라디언트만으로 암시하는데, 이는 순수 시각 효과라 보조 기술에 전혀 노출되지 않습니다.


❌ Don't#

포커스 가능한 콘텐츠 없이 감싸지 않기

// ❌ 자식 안에 탭 가능한 요소가 하나도 없음 — 키보드로 스크롤할 방법이 없다
FadeScroll(
  child: Text('그냥 텍스트만'),
)

뷰포트 자체는 포커스를 받지 않고 키도 처리하지 않습니다. 자식 콘텐츠 안에 링크·버튼 같은 포커스 가능한 요소가 없으면 키보드·스위치 사용자는 이 영역을 스크롤할 방법이 없습니다.

접근성 (Accessibility)#

역할 / Semantics#

양 플랫폼 모두 아무 역할도 내보내지 않습니다. Flutter 는 SingleChildScrollViewShaderMask(선택적으로 Scrollbar)만 조립하고 Semantics 를 감싸지 않습니다. Web 은 이름도 랜드마크도 없는 중첩된 <div> 두 개(루트 + 스크롤 뷰포트)를 emit 하며, 그 외 DOM 은 webkit 스크롤바를 숨기는 <style> 뿐입니다.

즉 이 스크롤 영역은 접근성 트리에서 식별되지도 이름 붙지도 않습니다.

키보드#

컴포넌트가 처리하는 키가 없습니다.

Flutter 의 화살표·PageUp 스크롤은 SDK 의 ScrollAction 이 현재 포커스 위치에서 디스패치되는 구조라, 포커스가 이미 자식 콘텐츠 안에 있을 때만 동작합니다. 이 컴포넌트는 아무것도 포커스 가능하게 만들지 않습니다. Web 의 onKeyDown / onKeyUp 은 기본값이 null 인 호출자 passthrough 속성이며 컴포넌트 자체는 어떤 키 핸들러도 등록하지 않습니다.

포커스#

포커스 관련 배선이 없습니다. Flutter 는 ScrollController 만 갖고 FocusNode / Focus / FocusTraversalGroup 이 없으며, Web 은 루트·뷰포트 어느 <div> 에도 tabindex 를 붙이지 않습니다. 포커스 링·트랩·복원 모두 없습니다.

스크린 리더#

자체적으로 읽히는 것이 없습니다. Flutter 는 자식 서브트리가 감싸는 노드 없이 그대로 읽히고, Web 은 이름 없는 일반 컨테이너 두 겹을 지나 자식 콘텐츠가 읽힙니다. 스크롤 가능한 영역이라는 사실도, 얼마나 남았는지도 안내되지 않습니다.

알려진 제약#

  • 뷰포트가 포커스를 받지 않고 키도 처리하지 않습니다. 자식 콘텐츠 안에 탭으로 이동할 수 있는 요소가 없다면, 키보드·스위치 사용자는 이 영역을 스크롤할 방법이 없습니다. 링크·버튼 같은 포커스 가능한 콘텐츠를 담거나, 소비자 쪽에서 스크롤 컨테이너를 포커스 가능하게 만들어야 합니다.
  • role="region" / aria-label 이 없어 스크롤 영역을 찾거나 이름으로 부를 수 없습니다. Web 은 passthrough attributes 로 붙일 수 있고, Flutter 는 FadeScrollSemantics 로 감싸야 합니다.
  • 기본값 showScrollbar: false 는 양 플랫폼에서 시각적 스크롤바를 숨깁니다. 이때 넘침을 알리는 단서는 페이드 그라디언트뿐인데 이는 순수 시각 효과라 보조 기술에 전혀 노출되지 않습니다. 스크롤 가능 여부가 중요한 맥락이라면 showScrollbar: true 를 검토하세요.

전역으로 적용되는 항목(감소된 모션·고대비·강제 색상 등)은 전역 접근성 축을 참고하세요.

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

항목FlutterWeb
클래스명FadeScrollFadeScroll
페이드 메커니즘 ShaderMask + LinearGradient CSS mask-image: linear-gradient(...)
스크롤 컨트롤 ScrollController (선택) native <div> scroll
Dynamic 페이드 ListenableBuilder 기반 ✅ JS measurer + CSS variable

빠른 오버라이드 (Chain)#

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

class FadeScrollChainExample extends StatelessWidget {
  const FadeScrollChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: CoreSpace.space200,
      child:
          FadeScroll(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                for (var i = 0; i < 20; i += 1)
                  Padding(
                    padding: EdgeInsets.symmetric(
                      vertical: CoreSpace.space8,
                      horizontal: CoreSpace.space16,
                    ),
                    child: Text('Item ${i + 1}').bodyMedium.onSurface,
                  ),
              ],
            ),
          ).withStyle(
            const CoreFadeScrollStyle(
              startOffset: CoreSpace.space80,
              endOffset: CoreSpace.space80,
              gradient: [
                CoreColor.token(CoreColors.primary),
                CoreColor.transparent,
              ],
            ),
          ),
    );
  }
}
class FadeScrollChainExample extends StatelessComponent {
  const FadeScrollChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        FadeScroll(
          child: div([
            for (var i = 0; i < 20; i += 1)
              div(
                [Text('Item ${i + 1}').bodyMedium.onSurface],
                classes: 'py-${CoreSpace.scale.space8} px-${CoreSpace.scale.space16}',
              ),
          ]),
        ).withStyle(
          const CoreFadeScrollStyle(
            startOffset: CoreSpace.space80,
            endOffset: CoreSpace.space80,
            gradient: [
              CoreColor.token(CoreColors.primary),
              CoreColor.transparent,
            ],
          ),
        ),
      ],
      styles: Styles(raw: {'height': '${CoreSpace.space200}px'}),
    );
  }
}