Gap
Row/Column 등 flex 레이아웃 자식 간격을 일관된 토큰으로 추가
목차
Row/Column (또는 Web의 flex) 레이아웃에서 자식 간격을 추가하는 유틸리티 컴포넌트입니다. 부모의 주축(main axis)을 자동 감지해 해당 축으로만 공간을 차지합니다.
CoUI는 세 가지 변형을 제공합니다:
- Gap — 고정 크기의 기본 간격
- MaxGap — 최대 크기까지만 차지하고 공간이 모자라면 양보하는 유연한 간격
-
SliverGap —
CustomScrollView안의 sliver 간격 (Flutter 전용 프로토콜; Web은 flex 스페이서로 대체)
기본 사용법 (Basic Usage)#
간격 값은 CoreSpace 토큰마다 하나씩 있는 Gap.spaceN() 이름 있는 생성자로 넣습니다 — 전부 const
이고, 내부에서 const CoreGapStyle(size: CoreSpace.spaceN) 을 접어줍니다.
// 수직 간격 (부모가 Column/flex-col일 때)
Column(
children: [
Text('제목'),
const Gap.space16(),
Text('내용'),
],
)
// 수평 간격 (부모가 Row/flex-row일 때)
Row(
children: [
Icon(LucideIcons.user),
const Gap.space8(),
Text('사용자 이름'),
],
)
// cross 축 / 색이 필요하면 gapStyle 슬롯으로
// (예: 4px 세로 spacer를 96px 가로로 강조)
const Gap(
gapStyle: CoreGapStyle(
size: CoreSpace.space4,
crossAxisExtent: CoreSize.size96,
),
)
// 색을 채워 디버그/구분선 목적
const Gap(
gapStyle: CoreGapStyle(
size: CoreSpace.space16,
color: CoreColor.token(CoreColors.error),
),
)
// cross 축을 무한대로 확장 (parent 전체 채우기)
Gap.expand(CoreSpace.space16)
Live Preview#
코드 보기
class GapDefaultExample extends StatelessComponent {
const GapDefaultExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
Gap.space16(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'inline-flex flex-row items-center',
);
}
}
class GapDefaultExample extends StatelessWidget {
const GapDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const Gap.space16(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- Row나 Column 내에서 자식 간 일관된 간격이 필요할 때
CoreSpace디자인 토큰 기반 간격을 사용하고 싶을 때SizedBox대안으로 의도가 분명한 간격 표현이 필요할 때
대신 다른 컴포넌트를 사용하세요:
Padding: 위젯 주변에 사방 여백을 추가할 때Spacer: Row/Column에서 남은 공간을 모두 채울 때- Divider: 빈 공간 대신 시각적 선이 필요할 때
MaxGap 사용법#
// 주축을 최대 space64까지만 차지, 공간 부족 시 양보
Row(
children: [
Button(variant: .primary, onPressed: () {}, child: Text('A')),
const MaxGap.space64(),
Button(variant: .primary, onPressed: () {}, child: Text('B')),
],
)
SliverGap 사용법#
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Text('Top')),
const SliverGap.space24(),
SliverToBoxAdapter(child: Text('Middle')),
const SliverGap(
gapStyle: CoreGapStyle(
size: CoreSpace.space24,
color: CoreColor.token(CoreColors.error),
),
),
SliverToBoxAdapter(child: Text('Bottom')),
],
)
Web은 sliver 프로토콜이 없으므로
SliverGap이Gap과 동일한 flex 스페이서로 렌더링됩니다. API는 동일합니다.
빠른 오버라이드 (Chain)#
이미 만든 Gap 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class GapChainExample extends StatelessWidget {
const GapChainExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
class GapChainExample extends StatelessComponent {
const GapChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'inline-flex flex-row items-center',
);
}
}
Props / Parameters#
Gap · MaxGap · SliverGap 셋 다 생성자 파라미터가 gapStyle 하나뿐입니다 — 모든 chrome (size
/ crossAxisExtent / color) 이 그 단일 슬롯으로 흐릅니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
gapStyle |
CoreGapStyle? |
null |
간격·교차축·색 단일 진입점 |
세 컴포넌트 모두 CoreGapStyle 을 공유합니다 (MaxGapStyle / SliverGapStyle 은 없습니다).
CoreGapStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
size |
double? |
Gap size along the parent's main axis (logical px).
null
inherits from the theme or the design-system default ([defaultSize]).
|
crossAxisExtent |
double? |
Gap size along the parent's cross axis (logical px).
null
leaves the cross axis to the parent — Web stretches (
align-self: stretch
), Flutter passes
RenderGap
a
0
extent the parent may stretch. No paired
default*
: both platforms branch on it being unset, so a default flips both — the stretch class goes away and every gap claims cross-axis space, painting a band wherever [color] is set. Cross-axis fill is opt-in through
Gap.expand
(
double.infinity
).
Deliberately has no default* — absence is the design.
Both platforms branch on absence, in opposite directions: Web emits
align-self: stretch
ONLY when null; Flutter passes RenderGap a 0 cross extent so the gap claims no cross-axis space. A default removes the stretch class and makes every gap in the kit claim space (painting a band wherever
color
is set). Cross-axis fill is opt-in via Gap.expand(double.infinity).
|
color |
CoreColor? |
Fill colour painted across the gap's rectangle.
null
keeps the gap transparent. No paired
default*
: Flutter returns early from
paint
and Web emits no
background-color
. A default would paint a rectangle in every gap in the kit —
Gap
is the spacer every other component composes.
Deliberately has no default* — absence is the design.
RenderGap.paint returns early on null (
if (fill == null) return
) and Web omits background-color. A default paints a rectangle in every gap in the kit — Gap is the spacer every other component composes.
|
이름 있는 생성자#
-
Gap.spaceN()/MaxGap.spaceN()/SliverGap.spaceN()—CoreSpace토큰마다 하나씩 있는const단축형 (space0~space256).const CoreGapStyle(size: CoreSpace.spaceN)을 접어줍니다. -
Gap.expand(mainAxisExtent, {color})/MaxGap.expand(...)—crossAxisExtent를double.infinity로 설정한 단축형.SliverGap에는 없습니다.
테마 (프로젝트 공통)#
세 컴포넌트가 각자 자기 테마 슬롯을 갖고, 모두 { style: CoreGapStyle? } 하나만 노출합니다 — CoreGapTheme
/ CoreMaxGapTheme / CoreSliverGapTheme.
CoreGapStyle.defaultSize
→ CoreGapTheme.style // 프로젝트 공통
→ widget.gapStyle // 인스턴스별
동작 스펙 (Behavior)#
size / crossAxisExtent는 논리 px 치수이며 자식·콜백·비활성 상태가 없는 간격 프리미티브입니다. 정해진 길이의 공간에는 Gap, 남은 flex 공간을 채우려면 Spacer, 실제 선이 필요하면 Divider 입니다.
방향 자동 감지#
별도 axis/direction 파라미터 없음. 부모의 flex/axis 방향을 자동으로 따라갑니다.
-
Flutter:
RenderGap이 부모RenderFlex.direction을 감지. 부모가 Flex가 아니면 enclosingScrollable의 axis를 fallback으로 사용. -
Web: CSS
flex-basis: ${size/16}rem+flex-shrink: 0+align-self: stretch→ 부모flex-direction에 따라row면width로,column이면height로 해석됨
렌더링#
-
Flutter:
LeafRenderObjectWidget→RenderGap(custom RenderBox). - Web:
<div>+ inline CSS (no Tailwind 클래스).
사용 가이드라인 (Usage Guidelines)#
✅ Do — 디자인 토큰 사용#
Column(
children: [
TitleWidget(),
const Gap.space16(),
BodyWidget(),
const Gap.space24(),
ActionWidget(),
],
)
❌ Don't — 매직 넘버 사용#
const Gap(gapStyle: CoreGapStyle(size: 13)) // 왜 13px?
const Gap(gapStyle: CoreGapStyle(size: 27)) // 일관성 없음
토큰 밖 값을 넣으려면 gapStyle 슬롯을 직접 열어야 하고, Gap.spaceN() 단축형은 애초에 토큰만 노출합니다.
❌ Don't — Padding 용도로 사용#
컨테이너 내부 여백은 Padding 또는 CSS p-* class 사용. Gap은 자식 간 간격 전용.
접근성 (Accessibility)#
역할#
없습니다 — Gap · MaxGap · SliverGap 세 컴포넌트 모두, 양 플랫폼 모두.
-
Flutter: 세 파일 어디에도
Semantics/CoUISemantics호출이 없습니다.Gap은LeafRenderObjectWidget인_RawGap을 반환할 뿐이고(gap.dart:254-267,gap.dart:283),MaxGap·SliverGap도 같은 형태입니다(max_gap.dart:256,sliver_gap.dart:237).RenderGap·RenderSliverGap어느 쪽도describeSemanticsConfiguration을 구현하지 않아 접근성 트리에 노드를 만들지 않습니다. -
Web: 자식 없는
<div>하나를 내보내고configureAttributes를 override 하지 않아role도aria-*도 붙지 않습니다(gap.dart:606-615,max_gap.dart:594,sliver_gap.dart:555).
키보드#
처리하는 키가 없습니다. 세 컴포넌트의 생성자가 받는 것은 gapStyle 과 — Web 한정 — id / classes
/ css / attributes / eventHandlers 뿐이고, 두 플랫폼 어느 파일에도 키 핸들러가 없습니다.
스크린 리더#
읽을 것이 없습니다. Web 은 콘텐츠도 역할도 없는 빈 요소이고 Flutter 는 노드 자체를 만들지 않으므로, 두 플랫폼의 구조 차이(Web 은 실제 <div>
가 하나 늘고 Flutter 는 늘지 않음)가 안내에는 나타나지 않습니다.
gapStyle.color 로 색을 채워도 마찬가지입니다 — 색은 Flutter 에서 RenderGap 의 채움으로, Web 에서 background-color
로 칠해질 뿐 이름도 역할도 만들지 않습니다.
포커스 관리#
포커스 대상이 아닙니다. Flutter 에 Focus / FocusNode 가 없고 Web 에 tabindex 가 없어 어느 플랫폼에서도 traversal 정지점이 되지 않습니다. 오버레이가 아니므로 진입 · 이탈 · 트랩 모두 해당 없음.
알려진 제약#
-
색을 채운
Gap은 구분선으로 읽히지 않습니다.CoreGapStyle.color는 사각형을 칠할 뿐 어느 플랫폼도separator역할이나 이름을 내지 않아, 시각적으로는 경계선인데 보조 기술에는 그 경계가 존재하지 않습니다. 구분이 의미를 갖는다면Divider(semantic: true)를 쓰세요 — Flutter 는 로컬라이즈된 라벨을 붙인Semantics를 내보내고(divider.dart:62-66), Web 은role="separator"와 같은 라벨을 내보냅니다(divider.dart:76-82).
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Gap / MaxGap / SliverGap | 동일 |
| 렌더링 | 자체 RenderGap / RenderSliverGap |
<div> + CSS flex-basis |
| 방향 감지 | 부모 Flex.direction → Scrollable axis | 부모 flex-direction (CSS) |
| 색 해석 | scheme.resolve(color).toValue() → Color |
color.toValue() → inline background-color |
| Sliver 지원 | RenderSliver 기반 | flex 스페이서로 대체 (API 동일) |