FadeScroll#
FadeScroll 은 스크롤 가능한 [child] 를 감싸고 그 가장자리에 그라디언트 페이드 마스크를 씌워 "더 많은 콘텐츠가 보이지 않는 영역에 있음" 을 시각적으로 암시합니다.
Live Preview#
class FadeScrollDefaultExample extends StatelessComponent {
const FadeScrollDefaultExample({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-2 px-4',
),
]),
),
],
styles: Styles(raw: {'height': '200px'}),
);
}
}
class FadeScrollDefaultExample extends StatelessWidget {
const FadeScrollDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: FadeScroll(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < 20; i += 1)
Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Text('Item ${i + 1}').bodyMedium.onSurface,
),
],
),
),
);
}
}
class FadeScrollHorizontalExample extends StatelessComponent {
const FadeScrollHorizontalExample({super.key});
@override
Component build(BuildContext context) {
return FadeScroll(
direction: .horizontal,
showScrollbar: true,
child: div(
[
for (var i = 0; i < 20; i += 1)
div(
[Text('Item ${i + 1}').bodyMedium.onSurface],
classes: 'flex items-center px-3 whitespace-nowrap',
),
],
classes: 'flex',
styles: Styles(raw: {'width': 'max-content'}),
),
);
}
}
class FadeScrollHorizontalExample extends StatelessWidget {
const FadeScrollHorizontalExample({super.key});
@override
Widget build(BuildContext context) {
return FadeScroll(
direction: .horizontal,
showScrollbar: true,
child: Row(
children: [
for (var i = 0; i < 20; i += 1)
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Center(
child: Text('Item ${i + 1}').bodyMedium.onSurface,
),
),
],
),
);
}
}
class FadeScrollWithScrollbarExample extends StatelessComponent {
const FadeScrollWithScrollbarExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
FadeScroll(
showScrollbar: true,
child: div([
for (var i = 0; i < 20; i += 1)
div(
[Text('Item ${i + 1}').bodyMedium.onSurface],
classes: 'py-2 px-4',
),
]),
),
],
styles: Styles(raw: {'height': '200px'}),
);
}
}
class FadeScrollWithScrollbarExample extends StatelessWidget {
const FadeScrollWithScrollbarExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: FadeScroll(
showScrollbar: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < 20; i += 1)
Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Text('Item ${i + 1}').bodyMedium.onSurface,
),
],
),
),
);
}
}
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'}),
);
}
}
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,
],
),
),
);
}
}
사용 시기 (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. |
gradient | a = |
동작 스펙 (Behavior)#
-
Flutter:
ListenableBuilder가controller위치를 listen → 스크롤 끝 도달 시 그쪽 페이드 자동 제거 (dynamic). -
Web:
mask-image+ JSFadeScrollMeasurer가onScroll마다 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 는 SingleChildScrollView → ShaderMask(선택적으로
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 은 passthroughattributes로 붙일 수 있고, Flutter 는FadeScroll을Semantics로 감싸야 합니다. -
기본값
showScrollbar: false는 양 플랫폼에서 시각적 스크롤바를 숨깁니다. 이때 넘침을 알리는 단서는 페이드 그라디언트뿐인데 이는 순수 시각 효과라 보조 기술에 전혀 노출되지 않습니다. 스크롤 가능 여부가 중요한 맥락이라면showScrollbar: true를 검토하세요.
전역으로 적용되는 항목(감소된 모션·고대비·강제 색상 등)은 전역 접근성 축을 참고하세요.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | FadeScroll | FadeScroll |
| 페이드 메커니즘 | ShaderMask + LinearGradient |
CSS mask-image: linear-gradient(...) |
| 스크롤 컨트롤 | ScrollController (선택) |
native <div> scroll |
| Dynamic 페이드 | ✅ ListenableBuilder 기반 |
✅ JS measurer + CSS variable |
관련 컴포넌트 (Related Components)#
- OverflowMarquee: 텍스트 자동 스크롤 + 양 끝 fade.
- ScrollableClient: fade 없는 단순 스크롤.
빠른 오버라이드 (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'}),
);
}
}