Gap | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (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가 아니면 enclosing Scrollable의 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)#

항목FlutterWeb
클래스명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 동일)
  • Join: 요소를 테두리 공유하며 밀착 배치
  • Divider: 간격 대신 시각적 구분선