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

OutlinedContainer

border·radius·padding·shadow·blur 토큰 chrome 으로 child 를 감싸는 박스 프리미티브 (인프라)

목차

child 를 토큰 기반 chrome(border + radius + optional padding + shadow + blur)으로 감싸는 박스 프리미티브(인프라)입니다. Card·Window·Alert·ChipInput 등 다수 컴포넌트가 이 박스 chrome 위에 쌓입니다. Web 은 <div> + Tailwind 유틸리티, Flutter 는 동일 CoreOutlinedContainerContract 필드를 읽어 양 플랫폼이 default 를 공유합니다.

Live Preview#

사용법#

OutlinedContainer(
  containerStyle: const CoreOutlinedContainerStyle(
    padding: CoreEdgeInsets.all(CoreSpace.space16),
  ),
  child: Text('테두리 컨테이너').bodyMedium,
)

Props / Parameters#

W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 슬롯 타입입니다.

속성타입기본값설명
child W? null chrome 안에 배치되는 콘텐츠
containerStyle CoreOutlinedContainerStyle? null backgroundColor·borderColor·borderWidth·borderRadius·padding·shadow·blur 단일 진입점
refraction CoreSurfaceRefraction? null 표면 굴절 요청. model 에 띠가 빛을 휘는 법칙(snell 기본 · paraxial)을 담습니다
role CoreSurfaceRole defaultRole 표면 재질을 선택하는 역할. 접근성 role이 아님

스타일 시스템 (Style System)#

CoreOutlinedContainerStyle 필드#

필드타입설명
surfaceStyle CoreSurfaceStyleId? Material override for this surface; null inherits the surrounding material. The material — translucency, backdrop blur, chroma lift, backdrop dimming, edge sheen and refraction — is a theme axis that reaches every surface at once. A value here replaces it for this instance alone: [CoreSurfaceStyleId.defaultStyle] is an opaque, stroke-bounded face inside a glass theme, [CoreSurfaceStyleId.liquidGlass] a glass face inside an opaque one. Palette, typography, geometry and density stay the surrounding theme's. [surfaceOpacity] and [surfaceBlur] tune the fill and the blur within whichever material applies. They cannot remove one, because neither reaches the sheen, the chroma lift or the backdrop dimming. Deliberately no default: null inherits the active theme material axis; a fixed default would override that axis before the resolver sees it.
backgroundColor CoreColor? Background fill colour override.
backgroundGradient CoreGradient? Background gradient fill override — painted instead of [backgroundColor] when set. The border and the shadow are unaffected; [surfaceOpacity] scales every stop's alpha. No default — absence is the design. Null means a solid [backgroundColor] fill, which both platforms branch on; a constant here would turn every surface built on this primitive into a gradient.
borderColorCoreColor?Border colour override.
borderRadius CoreBorderRadius? Border radius override.
borderWidth double? Border width override (logical px, pre-scaling).
preserveTransparentBorder bool? Whether a fully transparent border still reserves its stated width. The default collapses an invisible border. Set this when another state paints a border and its layout must not move during the transition. For dashed and dotted borders, opting in also reserves the width when the color is visible. A none border ignores this option.
borderStyle CoreOutlinedContainerBorderStyle? Border style override (solid / dashed / dotted / none).
dashLength double? Dash segment length when [borderStyle] is dashed (logical px). null → [defaultDashLength].
dashSpacing double? Gap between dash segments (logical px). null → [defaultDashSpacing].
elevation CoreOutlinedContainerElevation? Shadow elevation override. When [boxShadow] is also set, [boxShadow] wins.
boxShadow List<CoreShadowLayer>? Explicit shadow stack, overriding the [elevation]-derived one. The elevation enum can only name three prepared stacks, all of them soft ambient shadows. Some surface treatments are defined by shadows it cannot describe at all: a hard offset shadow has zero blur, and a raised-from-the-page look needs two layers going in opposite directions. This slot takes the stack directly so those are expressible without inventing a new enum member per look.
shadowBaseColor CoreColor? Shadow base colour override — the tint every layer of the shadow stack is drawn in, each layer's own opacity scaling its alpha. null defers to [defaultShadowBaseColor]. A token follows the active scheme. A raw colour pins the cast: it is the same in every mode, which is what a surface that stands for a physical object wants. On Web the pinned cast is written as the stack's literal layers instead of through the --shadow-* variables a surface style bends, so a style's shadow geometry reaches it on Flutter only.
shadowStrengthens bool? Whether a dark theme strengthens the stack this style states — the [boxShadow] or the [elevation] preset — by CoreShadow.darkStrength (capped at 1). null defers to [defaultShadowStrengthens]. false pins the stack's alpha at what each layer states, in every brightness: the cast is drawn the same on a light page and a dark one. That is for a surface that stands for a physical object photographed under one light ( MockupDevice ), not for a panel lifted off the page, whose cast has to read on a dark face too. The geometry a surface style bends is untouched — only the brightness's multiplier is left out. A cast the surface style contributes to a surface that states none ( controlShadow ) is the style's, not this stack, and keeps it.
padding CoreEdgeInsets? Inner padding override. Falls back to [defaultPadding] when null.
surfaceOpacity double? Background opacity multiplier (0.0 – 1.0). Multiplied into the alpha channel of [backgroundColor].
surfaceBlur double? Backdrop blur sigma (logical px). When > 0, applies a BackdropFilter (Flutter) / backdrop-filter: blur (Web).
width double? Explicit width override (logical px). No default, and must not have one. Null is not "width unknown", it is "size to the child" — both platforms branch on it rather than reading a value. Flutter hands the null straight to the container's width: , where it means unconstrained; Web only writes the width declaration inside if (width != null) . A constant here would pin every surface built on this primitive — Card , Alert , Window , every picker panel — to one width, and nothing in the tree asks for that.
height double? Explicit height override (logical px). No default, and must not have one — same branch as [width], on the block axis.
duration Duration? Animation duration for chrome property changes (color / radius / width / height). null or [Duration.zero] means no animation.
clipBehavior CoreClipBehavior? Clip behaviour override.

동작 스펙 (Behavior)#

이 프리미티브는 child를 둘러싼 chrome만 렌더하며 선택·클릭·비활성 상태를 소유하지 않습니다. 명시적 width·height는 논리 px 단위이고 생략하면 부모 제약과 자식 크기를 따릅니다. duration은 chrome 변경의 전환 시간(Duration)이며 clipBehavior는 모서리 밖 콘텐츠의 클리핑을 정합니다. dashed/dotted 테두리와 표면 흐림은 스타일에 따라 별도의 페인트 레이어로 합성됩니다.

표면 재질 (Material Override)#

표면의 재질(반투명 채움·배경 블러·채도·배경 어둡힘·가장자리 빛띠·굴절, 그리고 앱이 켠 서리와 몸체 음영)은 평소 테마 축이 모든 표면에 한 번에 정합니다. containerStyle.surfaceStyle 은 이 컨테이너 하나의 재질만 바꿉니다 — defaultStyle 은 유리 테마 안의 불투명한 면(평소의 외곽선 유지), liquidGlass 는 불투명 테마 안의 유리 면입니다. 팔레트·타이포·기하·밀도는 둘러싼 테마 그대로이고, null 은 둘러싼 재질을 그대로 씁니다. surfaceOpacity / surfaceBlur 는 재질 안에서 채움과 블러를 조절할 뿐 재질을 없애지 못합니다. 접근성 설정(투명도 줄이기·고대비)은 이 값보다 우선합니다.

OutlinedContainer(
  containerStyle: const CoreOutlinedContainerStyle(
    surfaceStyle: .defaultStyle,
  ),
  child: Text('불투명한 면'),
)

이 컨테이너를 합성하는 컴포넌트(예: Card)는 자기 스타일의 surfaceStyle 을 그대로 넘깁니다.

refraction 은 이 컨테이너 하나의 굴절 요청이고 법칙도 그 요청의 model 입니다. 앱의 모든 표면이 같은 법칙으로 휘게 하려면 surfaceTuning 의 refractionModel 로 정합니다 — 자세한 규칙과 두 법칙의 차이는 서페이스 스타일 가이드에 있습니다. 굴절은 요청이라서, 그리지 못하는 엔진에서는 법칙을 정해도 표면이 휘지 않고 나머지 재질만 그립니다.

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

제목·본문·미디어·액션 슬롯을 가진 완성된 콘텐츠 카드가 필요하면 Card를 선택합니다. 임의의 자식에 테두리와 표면만 얹는 저수준 조합에는 OutlinedContainer를 사용합니다.

✅ Do#

모든 chrome을 containerStyle 하나로 지정하기

OutlinedContainer(
  containerStyle: const CoreOutlinedContainerStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    padding: CoreEdgeInsets.all(CoreSpace.space16),
  ),
  child: Text('내용').bodyMedium,
)

background·border·radius·padding·shadow·blur는 모두 containerStyle 단일 슬롯 하나로 흐릅니다 — 바깥에 또 다른 Container/<div>로 같은 chrome을 덧그리면 박스가 이중으로 그려집니다.


❌ Don't#

role/이름이 필요한 자리에 그대로 쓰지 않기

// ❌ role/aria 없이 대화상자·영역처럼 쓰는 컨테이너로 오용
OutlinedContainer(
  child: importantDialogContent,
)

OutlinedContainer는 role도 이름도 포커스 대상도 내보내지 않는 순수 chrome 프리미티브입니다. 영역/대화상자 의미가 필요하면 소비자가 직접 Semantics/attributes로 감싸야 하고, 포커스 표시가 필요하면 FocusOutline을 별도로 합성해야 합니다.

접근성 (Accessibility)#

역할#

자체 role 이 없습니다. Flutter에는 role을 부여하는 Semantics 래퍼가 없으며 장식용 CustomPaint는 별도의 의미 노드를 만들지 않습니다. Web은 <div>에 호출자 속성만 싣고 dashed/dotted 테두리용 <svg>에 aria-hidden="true"를 붙입니다. landmark·region·group·aria-label 중 어느 것도 양 플랫폼에서 자동으로 나오지 않습니다.

읽는 사람에게 이는 이 박스가 접근성 트리에 존재하지 않는 것과 같다는 뜻입니다 — 이 표면에 이름이나 영역 의미가 필요하면 소비자가 직접 부여해야 합니다.

키보드#

처리하는 키가 없습니다. Web 은 호출자 eventHandlers 를 그대로 전달하지만 자체적으로는 아무 핸들러도 설치하지 않습니다.

포커스 관리#

포커스 대상이 아니며 어느 플랫폼에서도 traversal 정지점이 되지 않습니다. Flutter 에 FocusNode / Focus / autofocus 가 없고 Web 에 tabindex 가 없습니다. 포커스 링도 그리지 않습니다 — 이 컴포넌트의 style 은 border·radius·shadow·blur 만 해석합니다. 포커스 표시가 필요하면 FocusOutline 을 함께 쓰세요.

스크린 리더#

자기 몫으로 안내되는 것이 없고, 리더는 child로 통과합니다. Flutter의 장식용 CustomPaint는 의미 노드를 만들지 않고 Web의 SVG는 aria-hidden="true"이므로 테두리 스타일이 읽히지 않습니다.

알려진 제약#

  • 순수 chrome 이라 role·이름·설명·포커스 가능성이 전부 없습니다. 이 표면을 region / group / dialog 로 만들어야 한다면 소비자가 직접 주입해야 합니다.
  • 주입 통로가 플랫폼마다 다릅니다 — Web은 attributes: passthrough로 접근성 role / aria-*를 얹을 수 있습니다. Flutter에서는 Semantics로 감싸야 합니다. 생성자의 CoreSurfaceRole role은 재질 선택용이며 접근성 역할을 부여하지 않습니다.
  • 전환 애니메이션에 aria-busy 나 live region 처리가 없습니다 — Flutter 의 AnimatedContainer chrome 전환은 보조기술에 아무것도 알리지 않습니다. 전환의 모션 축 자체는 전역 접근성 축 에서 다룹니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
chrome 전환AnimatedContainer루트 CSS 전환
dashed/dotted 테두리 CustomPaint 레이어 aria-hidden SVG 테두리 레이어
표면 흐림합성된 SurfaceBlurCSS backdrop-filter
접근성 주입호출자가 Semantics로 감쌈루트 attributes로 전달
재질 오버라이드 surfaceStyle 의 재질을 컨테이너가 직접 그림 루트가 문서 마커 대신 coui-material-surface coui-material-<id> 를 달아 그 프리셋의 규칙만 선택됨
서브트리 단위 재질 SurfaceTheme 대응 없음 — 인스턴스 단위 surfaceStyle 만 있음
품질 적응 앱 루트의 adaptiveQuality 가 정한 단계가 재질에 접혀 들어옴 — 컨테이너는 단계를 읽지 않음 문서 속성 data-coui-quality 를 스타일시트( solid 의 불투명 채움)와 굴절 호스트( noRefraction 에서 굴절 필터를 뗌)가 읽음

Flutter와 Web 모두 dashed/dotted 테두리의 장식만 접근성 트리에서 제외하고 자식 콘텐츠는 유지합니다.

호출자가 refraction 을 직접 진술한 컨테이너는 품질 단계를 따르지 않습니다. 스타일의 진술이 아니라 호출자의 진술이라서입니다. 단계가 solid 이면 backdrop 패스가 없어 구부릴 뒤도 없습니다. 자세한 것은 서페이스 스타일 의 품질 적응을 보세요.