Frame | CoUI
LogoCoUI

Frame

Figma Frame 1:1 매핑 — Auto Layout · Sizing · Fill · Stroke · Effects 를 한 컴포넌트에서 표현하는 통합 레이아웃 프리미티브

Frame#

Frame 은 CoUI 의 통합 레이아웃 프리미티브입니다. Flutter 의 Container / Row / Column / Wrap / Padding / Center / Align / SizedBox / Expanded / DecoratedBox / Opacity / Transform / ClipRRect / Visibility 와 Web 의 <div> 를 한 컴포넌트로 흡수하여, Figma Frame 의 모든 "Inspect" 슬롯이 1:1 로 코드 prop 에 대응합니다.

Live Preview#

사용 시기 (When to Use)#

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

  • Figma 디자인을 Flutter / Web 어느 쪽으로든 코드로 옮길 때 — 디자인의 한 Frame 이 그대로 한 Frame 으로 매핑됩니다.
  • Auto Layout (direction · gap · alignment) 과 Sizing · Fill · Stroke · Effects 를 같이 다뤄야 할 때.
  • 단순 <div> / Container 를 raw 위젯으로 박지 않고 디자인 토큰 경유로 통일하고 싶을 때.

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

  • Card: 사용자 의미 있는 카드 단위 (header / body / footer 슬롯 포함) 가 필요할 때.
  • Stacks / Position: 자식들이 z 축으로 겹쳐야 하는 경우 (Flutter Stack 시맨틱).
  • Group: 절대 좌표로 자식을 배치해야 하는 경우.

기본 사용법 (Basic Usage)#

// 단일 자식 + chrome
Frame(
  padding: CoreEdgeInsets.all(CoreSpace.space16),
  backgroundColor: theme.colorScheme.surface,
  borderRadius: CoreBorderRadius.all(CoreRadius.box),
  child: Text('Inside frame'),
)

// Auto Layout (Row)
Frame(
  direction: CoreFrameDirection.row,
  gap: CoreSpace.space12,
  crossAxisAlignment: CoreCrossAxisAlignment.center,
  children: [icon, label],
)

// Sizing + flex (이 frame 자체가 부모 flex 의 자식)
Frame(
  flex: 1,
  fit: CoreFlexFit.tight,
  child: child,
)

// 효과 (elevation + dashed border)
Frame(
  elevation: CoreFrameElevation.medium,
  borderColor: theme.colorScheme.outline,
  borderWidth: CoreBorderWidth.medium,
  borderStyle: CoreFrameBorderStyle.dashed,
  child: Text('Dashed elevated'),
)

// SEO 시맨틱 태그 (Web). Flutter 는 Semantics role 로 매핑.
Frame(
  tag: 'main',
  child: pageBody,
)

Props / Parameters#

여기에는 위젯 생성자 파라미터만 적습니다. 시각 chrome 일부는 frameStyle (CoreFrameStyle) 슬롯으로도 흐릅니다 — 아래 레이어 표와 Style 표를 같이 보세요.

레이어 1 — 시각 / Figma#

속성타입설명
direction CoreFrameDirection? row / column / rowReverse / columnReverse / wrap . null 이면 flex가 아님 (block).
gapdouble?flex 자식 사이 간격 (logical px).
mainAxisAlignment CoreMainAxisAlignment? direction이 있을 때 주축 정렬.
crossAxisAlignment CoreCrossAxisAlignment? 교차축 정렬.
alignment CoreAlignment? directionnull일 때 9칸 그리드 정렬.
width / heightdouble?고정 크기.
minWidth / minHeight / maxWidth / maxHeight double? 최소/최대 제약.
flexint?부모 flex 안에서의 grow 가중치.
fit CoreFlexFit? tight (flex-basis: 0) / loose (고유 크기).
padding / margin플랫폼 타입안쪽 / 바깥 간격.
backgroundColor플랫폼 타입단색 배경.
gradientCoreGradient?선형/방사형 그라디언트.
backgroundImage CoreImageSource? 배경 이미지 (URL / asset).
borderColor / borderWidth / borderStyle / borderRadius 플랫폼 타입 스트로크. dashed / dotted 지원.
boxShadow / innerShadow 플랫폼 타입 임의 그림자. boxShadowelevation보다 우선.
layerBlur / backdropBlur double? 자체 / 배경 블러 sigma.
elevation CoreFrameElevation? none / soft / medium / strong 프리셋.
clipBehavior CoreClipBehavior overflow 클립 정책. 기본 hardEdge.
opacitydouble?0..1.
transform / transformAlignment 플랫폼 타입 / CoreAlignment? 2D / 3D 변환 + 변환 원점.
visible bool false면 레이아웃에서 제거. 기본 true.
frameStyle CoreFrameStyle? chrome / 치수 오버라이드 묶음 (아래 Style 표 참고).

레이어 2 — 동작#

속성타입설명
onTapvoid Function()?클릭 / 탭.
onHover CoreValueChanged<bool>? 마우스 호버 enter/leave.
onFocusCoreValueChanged<bool>?포커스 진입/이탈.
cursor CoreCursor? pointer / text / grab / … 마우스 커서.

레이어 3 — HTML / SEO (Web 우선, Flutter는 Semantics 매핑)#

속성타입설명
tag String Web에서 emit되는 HTML 태그. 기본 'div' . 'main' / 'nav' / 'section' / 'article' / 'header' / 'footer' / 'aside' 로 SEO 구조를 명시합니다. Flutter에서는 가장 가까운 Semantics role로 매핑됩니다.

레이어 4 — 접근성 / DOM 메타데이터#

속성타입설명
a11y CoreFrameA11y? role (ARIA), label ( aria-label / Semantics.label ), id (Web DOM id), attributes ( aria-* / data-* ). 일반 케이스는 null .

자식#

속성타입설명
child플랫폼 위젯?단일 자식.
children List<W>? 다중 자식. child와 동시에 지정할 수 없습니다.

스타일 시스템 (Style System)#

CoreFrameStyle 필드#

필드타입설명
backgroundColor CoreColor? Background colour override. No paired default* : the fill is opt-in. Flutter reads null in the hasChrome test (no fill and no AnimatedContainer wrapper) and Web emits no background-color . A default would give every frame both. Deliberately has no default* — absence is the design. Feeds Flutter's hasChrome test — the branch that decides whether an AnimatedContainer wraps the content AT ALL — and Web emits background-color only when set. A default gives every Frame a chrome wrapper plus a painted fill.
borderColor CoreColor? Border colour override — and the opt-in gate for the border itself. No paired default* : borderColor != null is what makes Flutter build a Border.all and Web add the border class (or mount the dashed SVG overlay). A default would stroke every frame, the plain layout ones included. [borderWidth] / [borderStyle] only shape a stroke this field has already opted into. Deliberately has no default* — absence is the design. It IS the border's opt-in gate: resolvedBorderColor != null is what builds Flutter's Border.all and adds Web's border class / mounts the dashed SVG overlay. A default strokes every Frame, including the plain layout ones.
borderWidth double? Border width (logical px). No paired default* : inside the [borderColor] gate, null means the platform's own thinnest stroke — Flutter Border.all(width: 0) (a hairline), Web the border utility's width with no inline override. A default would change both. The dashed overlay's CoreDashedBorder.strokeWidth fallback is branch-local: an SVG <rect> needs a concrete stroke. Deliberately has no default* — absence is the design. Inside the borderColor gate, null means the platform's thinnest stroke: Flutter paints Border.all(width: 0) (hairline), Web leaves the border utility's width alone and emits no inline border-width. A default changes the hairline AND adds an inline width to every bordered frame. (The dashed overlay's ?? CoreDashedBorder.strokeWidth is branch-local — lifting it would leak into the solid path.)
dashLength double? Dash segment length when borderStyle is dashed (logical px). null → [defaultDashLength].
dashSpacing double? Gap between dash segments (logical px). null → [defaultDashSpacing].
borderStyle CoreFrameBorderStyle? Border style — solid / dashed / dotted / none.
borderRadius CoreBorderRadius? Border radius. No paired default* : null is square corners and one of the hasChrome inputs on Flutter; Web emits no border-radius . A default would round every frame and wrap the plain layout ones in chrome. Deliberately has no default* — absence is the design. Another hasChrome input on Flutter; Web emits border-radius only when set. A default rounds every Frame and wraps the plain layout ones in chrome.
elevation CoreFrameElevation? Shadow elevation preset.
padding CoreEdgeInsets? Inner padding. No paired default* : Frame doubles as the plain layout primitive, so null means no padding wrapper — Flutter skips Padding , Web emits no rule. Even CoreEdgeInsets.zero would not be inert; it puts an inline padding on every frame's root. Deliberately has no default* — absence is the design. Flutter guards with resolvedPadding != null && != EdgeInsets.zero (skips the Padding widget); Web emits no padding rule. Even CoreEdgeInsets.zero is not inert — it would put an inline padding on every frame root, changing the DOM.
clipBehaviorCoreClipBehavior?Clip behaviour.
duration Duration? Animation duration for chrome property changes.
shadowBaseColor CoreColor? Shadow base tint override. null defers to [defaultShadowBaseColor].

Figma 매핑#

Figma 슬롯Frame prop
Auto Layout direction (Horizontal / Vertical / Wrap)direction
Auto Layout spacinggap
Auto Layout 정렬 (9-grid) mainAxisAlignment + crossAxisAlignment
Sizing (Fixed / Hug / Fill) width + height + flex (Fill 은 flex: 1, fit: tight )
Paddingpadding
Fill — SolidbackgroundColor
Fill — Linear / Radial gradientgradient
Fill — ImagebackgroundImage
Stroke (color / weight / style / radius) borderColor / borderWidth / borderStyle / borderRadius
Effects — Drop shadowboxShadow 또는 elevation 프리셋
Effects — Inner shadowinnerShadow
Effects — Layer blurlayerBlur
Effects — Background blurbackdropBlur
Layer opacityopacity
Transform (rotate / skew / scale)transform
Clip contentclipBehavior

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

✅ Do#

보더를 그리려면 borderColor를 명시적으로 지정

Frame(
  borderColor: theme.colorScheme.outline,
  borderWidth: CoreStrokeWidth.stroke1,
  borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
  child: content,
)

borderColornull이면 borderWidth/borderStyle을 지정해도 보더가 그려지지 않습니다 — 보더 렌더링은 borderColor 존재 여부로 게이트되는 양 플랫폼 공통 opt-in 방식입니다.


❌ Don't#

childchildren을 동시에 지정하지 않기

// ❌ child 와 children 동시 지정
Frame(
  child: Text('A'),
  children: [Text('A'), Text('B')],
)

contract 상 child를 지정하면 children은 반드시 null이어야 합니다(반대도 동일) — 둘 다 지정하면 어느 쪽이 렌더링될지 정의되지 않습니다.

✅ Do#

커스텀 그림자가 필요할 때만 boxShadow, 그 외엔 elevation 프리셋

Frame(
  elevation: CoreFrameElevation.medium,
  child: content,
)

elevation 프리셋은 4단계(none/soft/medium/strong)로 충분한 대부분의 경우에 토큰 경유로 일관된 그림자를 줍니다. 커스텀 그림자가 필요할 때만 boxShadow로 넘어가세요.


❌ Don't#

elevationboxShadow를 함께 지정해 둘 다 반영될 거라 기대하지 않기

// ❌ boxShadow 가 있으면 elevation 은 조용히 무시됨
Frame(
  elevation: CoreFrameElevation.strong,
  boxShadow: CoreShadow.lg,
  child: content,
)

boxShadow가 non-null이면 elevation은 아예 평가되지 않습니다 — 둘 중 하나만 선택해서 사용하세요.

접근성 (Accessibility)#

  • 일반 케이스는 a11ynull 로 두면 됩니다 — Frame 자체는 시맨틱 의미가 없는 컨테이너이므로 보조 기술이 자동으로 자식만 노출합니다.
  • 클릭 가능한 영역은 onTap 을 주면 자동으로 Semantics(button: true) (Flutter) / tabindex="0" (Web) 가 부여됩니다.
  • ARIA role 이 필요한 경우 a11y: CoreFrameA11y(role: 'navigation', label: 'Sidebar') 처럼 명시.
  • Web에서 tag: 'main' / 'nav' / 'section' / 'article' 등을 명시해야 검색 엔진과 스크린 리더가 페이지 구조를 정확히 파악합니다.

빠른 오버라이드 (Chain)#

이미 만든 Frame 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

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

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Frame(
          direction: CoreFrameDirection.row,
          gap: CoreSpace.space12,
          crossAxisAlignment: CoreCrossAxisAlignment.center,
          padding: const CoreEdgeInsets.all(CoreSpace.space16),
          children: const [
            Icon(
              LucideIcons.frame,
              iconStyle: CoreIconStyle(size: CoreSpace.space24),
            ),
            Text('Frame primitive'),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        Frame(
          direction: CoreFrameDirection.row,
          gap: CoreSpace.space12,
          crossAxisAlignment: CoreCrossAxisAlignment.center,
          padding: const CoreEdgeInsets.all(CoreSpace.space16),
          children: const [
            Icon(
              LucideIcons.frame,
              iconStyle: CoreIconStyle(size: CoreSpace.space24),
            ),
            Text('Full control'),
          ],
        ).withStyle(
          const CoreFrameStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class FrameChainExample extends StatelessComponent {
  const FrameChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Frame(
          direction: CoreFrameDirection.row,
          gap: CoreSpace.space12,
          crossAxisAlignment: CoreCrossAxisAlignment.center,
          padding: const CoreEdgeInsets.all(CoreSpace.space16),
          children: [
            Icon(
              LucideIcons.frame,
              iconStyle: CoreIconStyle(size: CoreSpace.space24),
            ),
            Text('Frame primitive'),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        Frame(
          direction: CoreFrameDirection.row,
          gap: CoreSpace.space12,
          crossAxisAlignment: CoreCrossAxisAlignment.center,
          padding: const CoreEdgeInsets.all(CoreSpace.space16),
          children: [
            Icon(
              LucideIcons.frame,
              iconStyle: CoreIconStyle(size: CoreSpace.space24),
            ),
            Text('Full control'),
          ],
        ).withStyle(
          const CoreFrameStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}
  • Card: header / body / footer 슬롯이 있는 의미 단위 카드
  • Group: 절대 좌표로 자식을 배치할 때
  • 스택 레이아웃: 자식이 z축으로 겹쳐야 할 때