Swiper#
자식 위젯을 감싸 스와이프 제스처에 반응하고, 화면 가장자리에서 오버레이 패널을 슬라이드 인하는 컴포넌트입니다. 모바일 환경의 자연스러운 스와이프 인터랙션으로 네비게이션 메뉴나 시트를 여는 데 활용됩니다.
Live Preview#
/// Default Swiper example for docs_site Web preview.
///
/// Mirrors the shadcn_flutter demo: pick the edge with the position
/// buttons, then swipe the card toward the opposite direction (or tap
/// Open) to reveal the drawer-style overlay. The open panel inherits
/// the composed Drawer's slide animation and can be swiped shut.
class SwiperDefaultExample extends StatefulComponent {
const SwiperDefaultExample({super.key});
@override
State<SwiperDefaultExample> createState() => _SwiperDefaultExampleState();
}
class _SwiperDefaultExampleState extends State<SwiperDefaultExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Component _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Component build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `justify-center` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides `h-full` gives
// the column room so the content centers; on content-sized
// top / bottom sides it resolves to auto and is a no-op.
overlayContent: div(
classes:
'flex h-full flex-col items-start justify-center '
'gap-${CoreSpace.scale.space8} p-${CoreSpace.scale.space16}',
[
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: div(
classes: 'flex w-full items-center justify-center',
styles: Styles(
raw: {'min-height': 'var(--coui-space-${CoreSpace.space256.toInt()})'},
),
[
Card(
child: div(
classes:
'flex flex-col items-center gap-${CoreSpace.scale.space12} '
'p-${CoreSpace.scale.space24}',
[
const Text('Swipe me!').titleMedium.onSurface,
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
],
),
);
}
}
/// Default Swiper example for docs_site Flutter preview.
///
/// Mirrors the shadcn_flutter demo: pick the edge with the position
/// buttons, then swipe the card toward the opposite direction (or tap
/// Open) to reveal the drawer-style overlay. The open panel inherits
/// the composed Drawer's slide animation and can be swiped shut.
class SwiperDefaultExample extends StatefulWidget {
const SwiperDefaultExample({super.key});
@override
State<SwiperDefaultExample> createState() => _SwiperDefaultExampleState();
}
class _SwiperDefaultExampleState extends State<SwiperDefaultExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Widget _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Widget build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `mainAxisAlignment` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides the column is
// forced tall so the alignment centers; on content-sized
// top / bottom sides the column wraps and it is a no-op.
overlayContent: Padding(
padding: const EdgeInsets.all(CoreSpace.space16),
child: Column(
crossAxisAlignment: .start,
mainAxisAlignment: .center,
mainAxisSize: .min,
spacing: CoreSpace.space8,
children: [
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: SizedBox(
width: double.infinity,
height: CoreSpace.space256,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(CoreSpace.space24),
child: Column(
mainAxisSize: .min,
children: [
const Text('Swipe me!').titleMedium.onSurface,
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
),
),
),
);
}
}
/// Sheet-variant Swiper example — minimal edge-to-edge surface.
///
/// Mirrors the shadcn_flutter demo: pick the edge with the position
/// buttons, then swipe the card toward the opposite direction (or tap
/// Open) to reveal the drawer-style overlay. The open panel inherits
/// the composed Drawer's slide animation and can be swiped shut.
class SwiperSheetExample extends StatefulComponent {
const SwiperSheetExample({super.key});
@override
State<SwiperSheetExample> createState() => _SwiperSheetExampleState();
}
class _SwiperSheetExampleState extends State<SwiperSheetExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Component _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Component build(BuildContext context) {
return Swiper(
variant: CoreSwiperVariant.sheet,
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `justify-center` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides `h-full` gives
// the column room so the content centers; on content-sized
// top / bottom sides it resolves to auto and is a no-op.
overlayContent: div(
classes:
'flex h-full flex-col items-start justify-center '
'gap-${CoreSpace.scale.space8} p-${CoreSpace.scale.space16}',
[
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: div(
classes: 'flex w-full items-center justify-center',
styles: Styles(
raw: {'min-height': 'var(--coui-space-${CoreSpace.space256.toInt()})'},
),
[
Card(
child: div(
classes:
'flex flex-col items-center gap-${CoreSpace.scale.space12} '
'p-${CoreSpace.scale.space24}',
[
const Text('Swipe me!').titleMedium.onSurface,
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
],
),
);
}
}
/// Sheet-variant Swiper example — minimal edge-to-edge surface.
///
/// Mirrors the shadcn_flutter demo: pick the edge with the position
/// buttons, then swipe the card toward the opposite direction (or tap
/// Open) to reveal the drawer-style overlay. The open panel inherits
/// the composed Drawer's slide animation and can be swiped shut.
class SwiperSheetExample extends StatefulWidget {
const SwiperSheetExample({super.key});
@override
State<SwiperSheetExample> createState() => _SwiperSheetExampleState();
}
class _SwiperSheetExampleState extends State<SwiperSheetExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Widget _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Widget build(BuildContext context) {
return Swiper(
variant: CoreSwiperVariant.sheet,
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `mainAxisAlignment` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides the column is
// forced tall so the alignment centers; on content-sized
// top / bottom sides the column wraps and it is a no-op.
overlayContent: Padding(
padding: const EdgeInsets.all(CoreSpace.space16),
child: Column(
crossAxisAlignment: .start,
mainAxisAlignment: .center,
mainAxisSize: .min,
spacing: CoreSpace.space8,
children: [
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: SizedBox(
width: double.infinity,
height: CoreSpace.space256,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(CoreSpace.space24),
child: Column(
mainAxisSize: .min,
children: [
const Text('Swipe me!').titleMedium.onSurface,
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
),
),
),
);
}
}
class SwiperChainExample extends StatefulComponent {
const SwiperChainExample({super.key});
@override
State<SwiperChainExample> createState() => _SwiperChainExampleState();
}
class _SwiperChainExampleState extends State<SwiperChainExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Component _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Component build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `justify-center` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides `h-full` gives
// the column room so the content centers; on content-sized
// top / bottom sides it resolves to auto and is a no-op.
overlayContent: div(
classes:
'flex h-full flex-col items-start justify-center '
'gap-${CoreSpace.scale.space8} p-${CoreSpace.scale.space16}',
[
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: div(
classes: 'flex w-full items-center justify-center',
styles: Styles(
raw: {'min-height': 'var(--coui-space-${CoreSpace.space256.toInt()})'},
),
[
Card(
child: div(
classes:
'flex flex-col items-center gap-${CoreSpace.scale.space12} '
'p-${CoreSpace.scale.space24}',
[
const Text('Swipe me!').titleMedium.onSurface,
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
],
),
).withStyle(
const CoreSwiperStyle(
dragHandleColor: CoreColor.token(CoreColors.primary),
dragHandleWidth: CoreSize.size64,
dragHandleThickness: CoreStrokeWidth.stroke6,
dragHandleBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
),
);
}
}
class SwiperChainExample extends StatefulWidget {
const SwiperChainExample({super.key});
@override
State<SwiperChainExample> createState() => _SwiperChainExampleState();
}
class _SwiperChainExampleState extends State<SwiperChainExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Widget _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Widget build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `mainAxisAlignment` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides the column is
// forced tall so the alignment centers; on content-sized
// top / bottom sides the column wraps and it is a no-op.
overlayContent: Padding(
padding: const EdgeInsets.all(CoreSpace.space16),
child: Column(
crossAxisAlignment: .start,
mainAxisAlignment: .center,
mainAxisSize: .min,
spacing: CoreSpace.space8,
children: [
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: SizedBox(
width: double.infinity,
height: CoreSpace.space256,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(CoreSpace.space24),
child: Column(
mainAxisSize: .min,
children: [
const Text('Swipe me!').titleMedium.onSurface,
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
),
),
),
).withStyle(
const CoreSwiperStyle(
dragHandleColor: CoreColor.token(CoreColors.primary),
dragHandleWidth: CoreSize.size64,
dragHandleThickness: CoreStrokeWidth.stroke6,
dragHandleBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 화면 가장자리 스와이프로 네비게이션 메뉴를 열 때
- 모바일 환경에서 제스처 기반으로 시트/패널을 열 때
- 버튼 탭과 스와이프 두 가지 방식 모두로 오버레이를 열고 싶을 때
대신 다른 컴포넌트를 사용하세요:
Drawer: 버튼/명령으로만 여는 가장자리 패널에 (제스처 불필요)Carousel: 스와이프로 슬라이드를 순환 탐색할 때
기본 사용법 (Basic Usage)#
// 좌측에서 열리는 드로어 스타일 오버레이
Swiper(
position: CoreSwiperPosition.left,
open: isMenuOpen,
onOpenChanged: (isOpen) => setState(() => isMenuOpen = isOpen),
overlayContent: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dashboard'),
Text('Settings'),
],
),
child: const AppHeader(),
)
// 아래에서 올라오는 시트 스타일
Swiper(
position: CoreSwiperPosition.bottom,
variant: CoreSwiperVariant.sheet,
open: isSheetOpen,
onOpenChanged: (isOpen) => setState(() => isSheetOpen = isOpen),
overlayContent: const ActionSheet(),
child: const PageContent(),
)
// 좌측에서 열리는 드로어 스타일 오버레이
Swiper(
position: CoreSwiperPosition.left,
open: isMenuOpen,
onOpenChanged: (isOpen) => setState(() => isMenuOpen = isOpen),
overlayContent: div([
const Text('Dashboard'),
const Text('Settings'),
], classes: 'flex flex-col gap-${CoreSpace.scale.space8}'),
child: const AppHeader(),
)
// 아래에서 올라오는 시트 스타일
Swiper(
position: CoreSwiperPosition.bottom,
variant: CoreSwiperVariant.sheet,
open: isSheetOpen,
onOpenChanged: (isOpen) => setState(() => isSheetOpen = isOpen),
overlayContent: const ActionSheet(),
child: const PageContent(),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget / Component |
필수 | 스와이프 제스처에 반응하는 자식 |
overlayContent |
Widget / Component |
필수 | 오버레이 패널에 표시할 콘텐츠 |
position |
CoreSwiperPosition |
left |
오버레이가 슬라이드 인하는 가장자리 |
variant |
CoreSwiperVariant |
drawer |
오버레이 표현 스타일 — drawer 는 열릴 때 앱 콘텐츠를 뒤로 축소(backdrop zoom-out)하고 그림자·보더·둥근 모서리를 가진 떠 있는 패널, sheet 는 backdrop 변형 없이 엣지에 붙는 최소 표면 (shadcn: "sheet 는 backdrop 변형이 없는 drawer") |
open |
bool |
false |
오버레이 열림 상태 (제어 값) |
onOpenChanged |
void Function(bool)? |
null |
열림 상태 변경 콜백 |
enabled |
bool |
true |
스와이프 제스처 활성화 여부 |
showDragHandle |
bool |
true |
드래그 핸들 표시 여부 — 핸들은 화면 중앙을 향한 엣지(잡아서 닫는 방향)에 붙음: bottom→위, top→아래, left/right→안쪽 엣지 세로 pill |
barrierDismissible |
bool |
true |
배경 탭으로 닫기 허용 여부 |
swiperStyle |
CoreSwiperStyle? |
null |
drag-handle 크롬 + nested
drawerStyle: CoreDrawerStyle?
(합성된 Drawer 패널의 크롬·치수·애니메이션·배리어) — chrome 단일 진입점
|
오버레이 패널은 통일
Drawer가 그립니다 — 슬라이드 애니메이션·배리어·드래그로 닫기를 그대로 상속하며, variant(drawer/sheet)는 그 패널에 얹히는 partialCoreDrawerStyle프리셋 + backdrop 변형 여부(drawer 만 앱 콘텐츠를 0.95 로 축소)입니다. 열린 패널을 가장자리 방향으로 스와이프하면 닫힙니다.스와이프-투-오픈은 드래그 1:1 추종입니다(shadcn 파리티): 축-우세 슬롭(18px)을 넘는 순간 패널이 마운트되어 손가락을 그대로 따라 나오고, 놓으면 절반(0.5) 임계 기준으로 스냅 — 이상이면 완전히 열리고(open 이벤트 발화), 미만이면 슬라이드-백되며 열림 이벤트 없이 사라집니다. fling 속도 게이트가 없어 느린 풀도 동일하게 동작합니다. 모든 닫힘(배리어 탭·Escape·드래그·× 버튼·프로그래매틱)은 닫힘 슬라이드 애니메이션을 재생합니다.
빠른 오버라이드 (Chain)#
이미 만든 Swiper 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class SwiperChainExample extends StatefulWidget {
const SwiperChainExample({super.key});
@override
State<SwiperChainExample> createState() => _SwiperChainExampleState();
}
class _SwiperChainExampleState extends State<SwiperChainExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Widget _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Widget build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `mainAxisAlignment` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides the column is
// forced tall so the alignment centers; on content-sized
// top / bottom sides the column wraps and it is a no-op.
overlayContent: Padding(
padding: const EdgeInsets.all(CoreSpace.space16),
child: Column(
crossAxisAlignment: .start,
mainAxisAlignment: .center,
mainAxisSize: .min,
spacing: CoreSpace.space8,
children: [
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: SizedBox(
width: double.infinity,
height: CoreSpace.space256,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(CoreSpace.space24),
child: Column(
mainAxisSize: .min,
children: [
const Text('Swipe me!').titleMedium.onSurface,
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
),
),
),
).withStyle(
const CoreSwiperStyle(
dragHandleColor: CoreColor.token(CoreColors.primary),
dragHandleWidth: CoreSize.size64,
dragHandleThickness: CoreStrokeWidth.stroke6,
dragHandleBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
),
);
}
}
class SwiperChainExample extends StatefulComponent {
const SwiperChainExample({super.key});
@override
State<SwiperChainExample> createState() => _SwiperChainExampleState();
}
class _SwiperChainExampleState extends State<SwiperChainExample> {
CoreSwiperPosition _position = CoreSwiperPosition.left;
bool _open = false;
Component _positionButton(CoreSwiperPosition position, String label) {
return Button(
variant: _position == position ? .primary : .outline,
size: .sm,
onPressed: () => setState(() => _position = position),
child: Text(label),
);
}
@override
Component build(BuildContext context) {
return Swiper(
position: _position,
open: _open,
onOpenChanged: (isOpen) => setState(() => _open = isOpen),
// Centered like the shadcn demo via `justify-center` —
// alignment is caller-owned (the drawer never centers content
// itself). On the full-height left / right sides `h-full` gives
// the column room so the content centers; on content-sized
// top / bottom sides it resolves to auto and is a no-op.
overlayContent: div(
classes:
'flex h-full flex-col items-start justify-center '
'gap-${CoreSpace.scale.space8} p-${CoreSpace.scale.space16}',
[
const Text('Dashboard'),
const Text('Settings'),
const Text('Profile'),
Button(
variant: .outline,
size: .sm,
onPressed: () => setState(() => _open = false),
child: const Text('Close'),
),
],
),
// The whole preview area is the swipe surface (the gesture layer
// covers the Swiper child) — the card just labels it.
child: div(
classes: 'flex w-full items-center justify-center',
styles: Styles(
raw: {'min-height': 'var(--coui-space-${CoreSpace.space256.toInt()})'},
),
[
Card(
child: div(
classes:
'flex flex-col items-center gap-${CoreSpace.scale.space12} '
'p-${CoreSpace.scale.space24}',
[
const Text('Swipe me!').titleMedium.onSurface,
Join(
children: [
_positionButton(CoreSwiperPosition.left, 'Left'),
_positionButton(CoreSwiperPosition.right, 'Right'),
_positionButton(CoreSwiperPosition.top, 'Top'),
_positionButton(CoreSwiperPosition.bottom, 'Bottom'),
],
),
Button(
onPressed: () => setState(() => _open = true),
child: const Text('Open swiper'),
),
],
),
),
],
),
).withStyle(
const CoreSwiperStyle(
dragHandleColor: CoreColor.token(CoreColors.primary),
dragHandleWidth: CoreSize.size64,
dragHandleThickness: CoreStrokeWidth.stroke6,
dragHandleBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
),
);
}
}
스타일 시스템 (Style System)#
CoreSwiperStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
drawerStyle |
CoreDrawerStyle? |
Nested chrome for the composed
Drawer
(panel surface / barrier / slide durations / drag-dismiss threshold …). Merges on top of the variant partial from [defaultsByVariant].
Deliberately has no flat
default*
— its baseline is [defaultsByVariant], which is this field's per-axis table.
Both resolvers read
defaultsByVariant[variant]!.merge(merged.drawerStyle)
, so the default is decided; it just varies along
variant
, which is where
core/layer-mapping.md
says a variant-dependent default belongs. The table is easy to miss with a name-matching search because its
value type
is this field's type, so no
drawerStyle:
key appears inside the literal. A flat constant could not represent it and would make things worse either way: empty, it is a no-op that adds a level to the merge chain for nothing; non-empty, it states panel chrome that [CoreSwiperVariant.sheet]'s partial (
panelBorderRadius:.zero
,
panelBorderWidth: 0
,
panelBoxShadow: []
) exists precisely to contradict — and any field it named that the sheet partial does not would silently reach the sheet too.
|
dragHandleColor |
CoreColor? |
Drag-handle indicator colour override. |
dragHandleWidth |
double? |
Drag-handle indicator width override. |
dragHandleThickness |
double? |
Drag-handle indicator thickness override. |
dragHandleInset |
double? |
Drag-handle inset padding override. |
dragHandleBorderRadius |
CoreBorderRadius? |
Drag-handle indicator corner radius override. |
CoreSwiperStyle 변형별 기본값 (CoreDrawerStyle)#
| 필드 | drawer | sheet |
|---|---|---|
panelBorderRadius | — | zero |
panelBorderWidth | — | 0 |
panelBoxShadow | — | [] |
변형 (Variants)#
드로어 (Drawer)#
그림자가 있는 입체 패널입니다. 네비게이션 메뉴에 적합합니다.
Swiper(
position: CoreSwiperPosition.left,
variant: CoreSwiperVariant.drawer,
open: isOpen,
onOpenChanged: (v) => setState(() => isOpen = v),
overlayContent: const NavigationMenu(),
child: const AppHeader(),
)
시트 (Sheet)#
그림자 없이 최소 장식의 패널입니다. 바텀 시트에 적합합니다.
Swiper(
position: CoreSwiperPosition.bottom,
variant: CoreSwiperVariant.sheet,
open: isOpen,
onOpenChanged: (v) => setState(() => isOpen = v),
overlayContent: const ActionSheet(),
child: const PageContent(),
)
동작 스펙 (Behavior)#
인터랙션#
- 스와이프 열기:
position방향을 드러내는 방향으로 스와이프하면 오버레이가 열림 (좌측 오버레이 → 오른쪽 스와이프) - 배경 탭 닫기:
barrierDismissible이true이면 배경 탭으로 닫힘 - 제어 값:
open과onOpenChanged로 버튼 등 외부 트리거로도 제어 가능
애니메이션#
- 슬라이드 전환: 300ms ease-in-out
- 배경 페이드: 200ms ease-in-out
사용 가이드라인 (Usage Guidelines)#
✅ Do#
open / onOpenChanged 로 controlled 상태를 유지하고, 스와이프 외 명시적 트리거도 항상 함께 제공
Swiper(
position: CoreSwiperPosition.left,
open: isMenuOpen,
onOpenChanged: (v) => setState(() => isMenuOpen = v),
overlayContent: const NavigationMenu(),
child: const AppHeader(),
)
// 스와이프 외에 버튼으로도 같은 상태를 열 수 있게
Button(
onPressed: () => setState(() => isMenuOpen = true),
child: const Icon(LucideIcons.menu),
)
스와이프 제스처는 키보드/스크린 리더 사용자가 실행할 수 없습니다 — open/onOpenChanged 로 상태를 노출해야 버튼 같은 명시적 트리거로도 같은 오버레이를 열고 닫을 수 있습니다.
❌ Don't#
콘텐츠 성격과 반대로 variant 를 고르지 않기
// ❌ 네비게이션 메뉴에 sheet — backdrop 변형·그림자 없이 밋밋하게 붙어
// "떠 있는 패널" 위계가 사라짐
Swiper(
position: CoreSwiperPosition.left,
variant: CoreSwiperVariant.sheet,
overlayContent: const NavigationMenu(),
child: const AppHeader(),
)
drawer 는 backdrop zoom-out + 그림자로 "떠 있는 패널"을 표현하고, sheet 는 backdrop 변형 없이 엣지에 붙는 최소 표면입니다 — 콘텐츠 성격과 반대로 고르면 사용자가 기대하는 시각적 위계가 어긋납니다.
접근성 (Accessibility)#
- 오버레이 패널에
role="dialog"+aria-modal="true"부여 - 배경 닫기 버튼에
aria-label제공 - 스와이프 외에 버튼 등 명시적 트리거를 함께 제공하여 키보드/스크린 리더 사용자도 오버레이를 열 수 있게
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Swiper | Swiper |
| 제스처 감지 | GestureDetector 드래그 | touch / pointer 이벤트 |
| 오버레이 마운트 | PopupRoute | position: fixed 패널 |
| 애니메이션 | SlideTransition | CSS transition: transform |