LogoSkills

coui-dock

하단 내비게이션 바, 탭 독, 앱 하단 메뉴를 CoUI Flutter(coui_flutter) 또는 CoUI Web(coui_web)에서 Dock 위젯과 CoreDockItemData 아이템, CoreDockSize(xs-xl), CoreDockStyle, CoreDockTheme로 만들 때 활성화합니다.

CoUI Dock#

Quick Reference#

  • Widget: Dock
  • Variant: 없음 (variant 파라미터 없음) — Dock은 단일 비주얼 아이덴티티만 가집니다
  • Size: CoreDockSize — xs/sm/md/lg/xl (default md); xs/sm은 라벨 숨김
  • Style slot: dockStyle: CoreDockStyle(...) — 색상/높이/패딩/아이콘·라벨·간격 슬롯이 모두 이 하나의 composite slot을 통해 전달됩니다
  • Canonical snippet:
Dock(
  items: [
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.house),
      label: 'Home',
      isActive: true,
      onPressed: () {},
    ),
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.search),
      label: 'Search',
      onPressed: () {},
    ),
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.settings),
      label: 'Settings',
      onPressed: () {},
    ),
  ],
)

한마디로#

독(Dock)은 화면 맨 아래에 붙는 "하단 메뉴 바"입니다. 스마트폰 앱 아래쪽에 늘 보이는 홈·검색·내 정보 같은 아이콘 줄을 떠올리면 됩니다. 이 문서는 그 하단 메뉴 바를 우리 앱(모바일)과 웹사이트에서 똑같은 모양·규칙으로 만드는 방법을 정리한 설명서입니다. 메뉴 항목(아이콘 + 이름)만 목록으로 넘겨주면 나머지 모양은 알아서 통일되게 그려집니다.

무엇을·언제#

  • 무엇을 해주나요: 화면 하단에 고정되는 메뉴 바를 만들어 줍니다. 각 항목은 아이콘과 (선택적으로) 짧은 이름표로 구성되고, 지금 보고 있는 화면의 항목은 강조색으로 표시됩니다.
  • 언제 쓰이나요: 앱의 큰 화면들(홈/검색/알림/내 정보 등)을 오가는 "메인 이동 수단"이 필요할 때 사용합니다. 항목을 누르면 해당 화면으로 이동합니다.
  • 크기를 고를 수 있습니다: 바의 높이를 5단계(아주 낮음~아주 높음) 중에서 고릅니다. 아주 낮은 두 단계(xs·sm)에서는 공간을 아끼려고 이름표가 자동으로 숨고 아이콘만 보입니다.
  • 한 번 정해두면 통일됩니다: 앱과 웹 양쪽에서 같은 방식으로 만들어지고, 프로젝트 전체의 기본 모양(배경색·강조색 등)을 한곳에서 정해둘 수도 있습니다.

핵심 용어#

용어쉬운 설명
Dock화면 맨 아래에 고정되는 메뉴 바 (하단 내비게이션)
item (항목)메뉴 바 속 버튼 하나 — 아이콘 + 이름표(선택) 묶음
icon항목의 그림 (집 모양, 돋보기 등)
label아이콘 아래에 붙는 짧은 이름표 (예: "홈")
isActive지금 선택된 항목 표시 — 강조색과 굵은 글씨로 보입니다
onPressed항목을 눌렀을 때 실행할 동작 (보통 화면 이동)
size바의 높이 단계 (xs 아주 낮음 ~ xl 아주 높음). xs·sm에서는 이름표가 숨습니다
dockStyle독 하나만 살짝 다르게(색·높이·간격 등) 꾸미는 세부 설정
semanticLabel화면 낭독기(시각장애인 보조 기능)가 읽어주는 이 메뉴 바의 이름
Flutter모바일/앱 화면을 만드는 기술
Web웹 브라우저용 화면을 만드는 기술
theme / theming프로젝트 전체에 적용되는 기본 모양 설정
Legacy / Deprecated옛날 방식이라 새 코드에서는 쓰지 말아야 하는, 곧 사라질 항목

Overview#

Dock is the unified bottom dock navigation bar. It takes a data-only items list (CoreDockItemData<W>) and renders each entry itself — there is no separate item widget type. Items are laid out in a row with spaceAround distribution; each item is an icon with an optional label below it. Active items are tinted with the primary color and get a heavier label weight.

  • No variant enum — Dock has one visual identity.
  • Size: CoreDockSizexs, sm, md, lg, xl (default md). Note: this is CoreDockSize, not CoreComponentSize. The size preset drives the bar height and default icon size, and gates label visibility: xs/sm hide labels (icon-only), md/lg/xl show them (CoreDockStyle.labelVisibleFor(size)).
  • Style slot: dockStyle: CoreDockStyle(...) — all chrome (colors, height, padding, icon/label/gap slots) flows through this single composite slot.
  • size is a widget-only semantic identifier — the theme cannot override it.

Item data — CoreDockItemData<W> (shared contract)#

Both platforms take the same data class from coui_core; only the icon widget type W differs (Widget on Flutter, Component on Web).

FieldTypeDefaultNotes
icon W required Icon widget/component rendered at the top of the item
label String? null Plain string — the dock renders the Text itself. Hidden automatically on xs / sm
isActive bool false Selected state: activeColor tint + semiBold label
onPressed void Function()? null Tap/click callback; when null the item is inert but still renders

Flutter (coui_flutter)#

Import#

import 'package:coui_flutter/coui_flutter.dart';

Single barrel only — never import from src/.

Basic usage#

Dock(
  items: [
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.house),
      label: 'Home',
      isActive: true,
      onPressed: () {},
    ),
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.search),
      label: 'Search',
      onPressed: () {},
    ),
    CoreDockItemData<Widget>(
      icon: const Icon(LucideIcons.settings),
      label: 'Settings',
      onPressed: () {},
    ),
  ],
)

Dock is a StatelessWidget (CoreDockContract<Widget>). Icon size and per-state tint are injected via IconTheme — do not set them on the Icon manually. The label Text is created by the dock from the plain label string.

Sizes#

Dock(size: CoreDockSize.xs, items: myItems)  // 48 high, icon-only
Dock(size: CoreDockSize.sm, items: myItems)  // 56 high, icon-only
Dock(size: CoreDockSize.md, items: myItems)  // 64 high, labels shown (default)
Dock(size: CoreDockSize.lg, items: myItems)  // 72 high, labels shown
Dock(size: CoreDockSize.xl, items: myItems)  // 80 high, labels shown

All dimensions (height, icon size, horizontal padding) are multiplied by theme.scaling at resolve time.

Selected-tab pattern (stateful)#

class MainShell extends StatefulWidget {
  const MainShell({super.key});

  @override
  State<MainShell> createState() => _MainShellState();
}

class _MainShellState extends State<MainShell> {
  int _index = 0;

  static const _tabs = [
    (LucideIcons.house, 'Home'),
    (LucideIcons.search, 'Search'),
    (LucideIcons.user, 'Profile'),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(child: _buildPage(_index)),
        Dock(
          items: [
            for (var i = 0; i < _tabs.length; i++)
              CoreDockItemData<Widget>(
                icon: Icon(_tabs[i].$1),
                label: _tabs[i].$2,
                isActive: i == _index,
                onPressed: () => setState(() => _index = i),
              ),
          ],
        ),
      ],
    );
  }

  Widget _buildPage(int index) => Text(_tabs[index].$2);
}

Accessibility#

The dock wraps itself in a Semantics container. semanticLabel defaults to 'navigation' when not provided:

Dock(
  semanticLabel: '메인 내비게이션',
  items: myItems,
)

Per-instance style (dockStyle)#

Dock(
  dockStyle: const CoreDockStyle(
    backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
    activeColor: CoreColor.token(CoreColors.secondary),
    height: 60,
  ),
  items: myItems,
)

Item tap affordance: items with a non-null onPressed get a click cursor (MouseRegion) and an opaque GestureDetector hit area covering the icon+label column.

Web (coui_web / Jaspr)#

Import#

import 'package:coui_web/coui_web.dart';

The barrel re-exports Jaspr and coui_core, so no separate imports are needed.

Basic usage#

Same API; the item icon is a Jaspr Component. The label stays a plain String on both platforms (the dock renders the text itself).

Dock(
  size: CoreDockSize.md,
  items: [
    CoreDockItemData(
      icon: const Icon(LucideIcons.house),
      label: 'Home',
      isActive: true,
      onPressed: () {},
    ),
    CoreDockItemData(
      icon: const Icon(LucideIcons.settings),
      label: 'Settings',
      onPressed: () {},
    ),
  ],
)

Renders a <div> with role="navigation" and Tailwind classes (flex flex-row w-full items-center justify-around border-t plus token color classes); height and horizontal padding go inline as rem values. Each item is a <div> column (flex flex-col items-center justify-center select-none) with cursor-pointer when clickable and a DOM click handler wired to onPressed. Token colors emit dark-mode-reactive classes; raw overrides go inline.

Accessibility (Web)#

role="navigation" is always set; aria-label is added only when semanticLabel is provided (unlike Flutter, there is no 'navigation' default string):

Dock(
  semanticLabel: 'Primary',
  items: myItems,
)

Web-only constructor extras#

The Web constructor also accepts id, classes, css (Jaspr Styles), attributes, and eventHandlers from the UiComponent base; user classes are appended after the resolved class string and user css combines after the resolved inline styles. The component also provides copyWith(...).

Dock(
  classes: 'fixed bottom-0 left-0',
  items: myItems,
)

dockStyle (shared style)#

Web reuses the same CoreDockStyle from coui_core:

Dock(
  dockStyle: const CoreDockStyle(
    borderColor: CoreColor.token(CoreColors.outlineVariant),
  ),
  items: myItems,
)

Parameters#

ParameterType (Flutter)Type (Web)DefaultNotes
items List<CoreDockItemData<Widget>> List<CoreDockItemData<Component>> required Rendered leading → trailing
size CoreDockSize? CoreDockSize? null (→ md) xs / sm / md / lg / xl; widget-only semantic identifier
semanticLabel String? String? null Flutter falls back to 'navigation'; Web omits aria-label when null
dockStyle CoreDockStyle? CoreDockStyle? null Per-instance composite chrome/slot style

Web-only: id, classes, css (Jaspr Styles), attributes, eventHandlers, plus copyWith(...).

Size token table#

CoreDockSize — full set. Per-size defaults come from CoreDockStyle.defaultsBySize (single source of truth shared by Flutter and Web; values pre-scaling):

SizeBar heightIcon sizeLabels
xs 48 (size48) 20 (size20) hidden
sm 56 (size56) 22 (CoreDockStyle.smIconSize) hidden
md (default) 64 (size64) 24 (size24) shown
lg 72 (size72) 28 (size28) shown
xl 80 (size80) 32 (size32) shown

Label visibility is queried via CoreDockStyle.labelVisibleFor(size) (xs/smfalse). The sm icon size (22) is not on the CoreSize scale, so it is exposed as the style-level constant smIconSize.

Single style slot — CoreDockStyle#

Constructor fields (all optional):

FieldTypePurpose / Default
height double? Bar height (pre-scaling); falls back to defaultsBySize[size]
padding CoreEdgeInsets? Bar padding; default CoreEdgeInsets.symmetric(horizontal: CoreSpace.space8) — the resolver applies it horizontally
itemIconLabelGapStyle CoreGapStyle? Nested gap slot between icon and label; default CoreGapStyle(size: CoreSpace.space4) , forwarded to Gap(gapStyle: …)
backgroundColor CoreColor? Bar fill; default surface token
borderColor CoreColor? Top border color; default outline token
activeColor CoreColor? Active item foreground (icon + label); default primary token
inactiveColor CoreColor? Inactive item foreground; default onSurfaceVariant token
labelTextStyle CoreTextStyle? Overlay on the label role — applies to both active and inactive labels
iconStyle CoreIconStyle? Icon size + color override (color otherwise inherits the item foreground)

Label typography defaults: labelSmall role for both states — active adds semiBold weight (defaultActiveLabelStyle), inactive adds medium weight (defaultInactiveLabelStyle); the per-state foreground color is injected by the resolver. CoreDockStyle also exposes merge, copyWith, and the named defaults as statics: defaultPadding, defaultItemIconLabelGapStyle, defaultIconStyle, defaultBackgroundColor, defaultBorderColor, defaultActiveColor, defaultInactiveColor, defaultActiveLabelStyle, defaultInactiveLabelStyle, defaultsBySize, smIconSize, labelVisibleFor.

Dock(
  dockStyle: const CoreDockStyle(
    itemIconLabelGapStyle: CoreGapStyle(size: CoreSpace.space2),
    iconStyle: CoreIconStyle(size: 26),
  ),
  items: myItems,
)

Project-level theming#

CoreDockTheme has exactly one slot — style. There is no variant enum, hence no variantStyles; and size is widget-only, so the theme cannot set it. Resolve chain:

design-system default (CoreDockStyle.defaultX / defaultsBySize[size])CoreDockTheme.style                       // 프로젝트 공통
  → parent component slot override
  → widget.dockStyle                          // 인스턴스별
const CoreDockTheme(
  style: CoreDockStyle(
    backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
    activeColor: CoreColor.token(CoreColors.secondary),
  ),
)

Register it on the app theme's coreComponentTheme.dock slot — both platform resolvers read theme.coreComponentTheme?.dock.

Flutter ↔ Web Differences#

AspectFlutterWeb
Widget class Dock (StatelessWidget, CoreDockContract<Widget>) Dock (UiComponent, CoreDockContract<Component>)
Item icon type Widget (e.g. Icon(LucideIcons.house)) Component (e.g. Icon(LucideIcons.house))
Item label / onPressed String? / void Function()? (shared CoreDockItemData) same — identical data class
Render SemanticsContainer (height, bg, top border) → Row(spaceAround) <div role="navigation"> with Tailwind classes + inline rem styles
A11y label Semantics(label: semanticLabel ?? 'navigation') aria-label only when semanticLabel != null (no default)
Item interaction GestureDetector.onTap + MouseRegion click cursor DOM click event + cursor-pointer class
Scaling theme.scaling multiplies height / icon / padding px → rem conversion
Extra ctor params id , classes , css , attributes , eventHandlers ; also copyWith(...)

Shared on both: CoreDockItemData, CoreDockSize (default md), CoreDockStyle (identical fields + defaultsBySize), CoreDockTheme (single style slot), xs/sm label hiding.

Common mistakes#

  • Passing CoreComponentSize to size — the dock uses its own CoreDockSize enum (same five names, different type).
  • Wrapping the item label in a Text/text(...)label is a plain String; the dock renders the text node itself.
  • Setting icon color/size on the Icon directly — use dockStyle.iconStyle (or leave it to the active/inactive foreground) so the per-state tint keeps working.
  • Expecting labels on xs/sm — those sizes hide labels by design; use md+ if labels must show.

Pitfalls#

  • size takes CoreDockSize, not CoreComponentSize — same five names (xs/sm/md/lg/xl) but a different, dock-only type; passing the wrong enum is a common mistake.
  • label is a plain String, not a Text/text(...) node — the dock renders the text itself internally, so wrapping it yourself is redundant/wrong.
  • xs/sm hide labels automatically (icon-only) by design; labels only show at md/lg/xl — this isn't configurable per instance, only via size choice.
  • Accessibility default differs by platform: Flutter's Semantics falls back to 'navigation' when semanticLabel is omitted, but Web only emits aria-label when semanticLabel is explicitly provided — there is no Web default string.
  • size is a widget-only semantic identifier — CoreDockTheme cannot override it, even though it can override every other visual aspect via its single style slot.
  • Don't set icon color/size directly on the Icon — use dockStyle.iconStyle (or leave it alone) so the active/inactive foreground tint injected via IconTheme keeps working.

v0.127 Migration (Legacy)#

v0.127 is a de-prefix hard break: the widget class was renamed and no Co-prefixed alias remains. Core* contract names (CoreDockItemData, CoreDockSize, CoreDockStyle, CoreDockTheme, CoreDockContract) are unchanged.

❌ Legacy (pre-0.127, removed)✅ v0.127
CoDock(items: …)Dock(items: …)
CoLucideIcons.houseLucideIcons.house
CoRadixIcons.*RadixIcons.*
CoIcon(…) (item icons)Icon(…)
CoGap(…) (icon↔label spacer internals)Gap(…)

Additional migration notes:

  • ❌ Legacy Web docs rendered the dock with DaisyUI dock-* classes — v0.127 renders with CoUI design tokens / Tailwind classes instead; the DaisyUI class hooks are gone.
  • The xs/sm label hiding intentionally matches the legacy visual behavior, so no layout change is expected when migrating those sizes.
  • Import stays the single barrel (package:coui_flutter/coui_flutter.dart / package:coui_web/coui_web.dart).