LogoSkills

coui-popup

메뉴, 드롭다운, 툴팁 유사 오버레이 뒤에 있는 떠다니는 패널 표면을 CoUI Flutter(coui_flutter) 또는 CoUI Web(coui_web)에서 Popup 위젯과 children, open, direction(CoreAxis.vertical/horizontal), CorePopupStyle, CorePopupTheme로 만들 때 활성화...

CoUI Popup#

Quick Reference#

  • Widget: Popup
  • Variant: 없음 (variant 파라미터 없음)
  • Size: 없음 (CoreComponentSize 미적용)
  • Style slot: popupStyle: CorePopupStyle(...)
  • Canonical snippet:
Popup(
  children: const [
    Text('Profile'),
    Text('Settings'),
    Text('Sign out'),
  ],
)

한마디로#

팝업(Popup)은 화면 위에 살짝 떠 있는 "쟁반"입니다. 메뉴를 눌렀을 때 나오는 선택지 목록, 드롭다운, 우클릭 메뉴 같은 것들이 모두 이 쟁반 위에 담겨서 나옵니다. 이 문서는 그 떠 있는 쟁반(테두리·그림자·둥근 모서리가 있는 패널)을 앱과 웹에서 똑같은 모양으로 만드는 방법을 정리한 설명서입니다. 쟁반 자체만 다루고, 쟁반을 "어디에 띄울지"는 메뉴·드롭다운 같은 상위 부품이 정합니다.

무엇을·언제#

  • 무엇을 해주나요: 메뉴/드롭다운/작은 떠 있는 창의 "겉면 패널"을 만들어 줍니다. 흰 배경, 얇은 테두리, 둥근 모서리, 은은한 그림자가 자동으로 적용됩니다.
  • 내용은 자유롭게: 쟁반 안에 들어갈 항목들(메뉴 항목, 구분선 등)을 목록으로 넣으면 세로 또는 가로로 정렬해 줍니다.
  • 보이기/숨기기: 켜고 끄는 스위치(open)가 있어서, 끄면 화면에서 완전히 사라집니다.
  • 언제 쓰이나요: 드롭다운 메뉴, 우클릭(컨텍스트) 메뉴, 메뉴바의 하위 메뉴 등 "떠 있는 패널"이 필요한 모든 곳의 밑바탕으로 쓰입니다. 보통은 상위 부품(드롭다운 등)이 알아서 사용하지만, 직접 커스텀 떠 있는 패널을 만들 때도 씁니다.
  • 주의: 이 부품은 "패널 겉면"만 담당합니다. 화면 어느 위치에 띄울지, 바깥을 눌렀을 때 닫히는 동작 등은 이 부품이 아니라 상위 부품의 몫입니다.

핵심 용어#

용어쉬운 설명
Popup메뉴·드롭다운이 담기는, 떠 있는 패널(쟁반)
children쟁반 위에 올라가는 내용물 목록 (메뉴 항목, 구분선 등)
open쟁반을 보일지(켬) 숨길지(끔) 정하는 스위치
direction내용물을 세로로 쌓을지(vertical) 가로로 늘어놓을지(horizontal)
popupStyle이 쟁반 하나만 살짝 다르게(색·모서리·그림자·너비 등) 꾸미는 세부 설정
minWidth쟁반이 너무 좁아지지 않도록 정하는 최소 너비
boxShadow쟁반이 떠 있어 보이게 하는 그림자 효과
theme (CorePopupTheme)프로젝트 전체 팝업 패널에 공통 적용하는 기본 모양 설정
Flutter모바일/앱 화면을 만드는 기술
Web웹 브라우저용 화면을 만드는 기술
Legacy / Deprecated옛날 방식이라 새 코드에서는 쓰지 말아야 하는, 곧 사라질 항목

Overview#

Popup is the unified styled floating-panel container on both CoUI Flutter and CoUI Web (as of v0.127 the class is bare Popup — no prefix, no alias). It is the chrome surface for floating content — menus, dropdowns, context menus, menubar submenus — laying its children out along direction inside a bordered, elevated, rounded panel, shown/hidden via the open flag.

Key facts (from the CorePopupContract shared by both platforms):

  • No variant, no size. Popup carries no semantic variant/size enums — its only semantic parameter is direction: CoreAxis (vertical default, horizontal). All appearance flows through the single style slot.
  • Single style slot: popupStyle: CorePopupStyle — padding, corner radius, border width/color, background, elevation shadow (+ shadow base color), and minimum width.
  • Panel only. Popup does not anchor, position, or dismiss itself. Overlay placement and outside-tap handling belong to the consuming components (dropdown menu, context menu, menubar, navigation menu — their resolvers consume the popup's resolved style so derived chrome stays in lock-step).

Flutter (coui_flutter)#

Import#

import 'package:coui_flutter/coui_flutter.dart';

Single barrel only — never import from src/.

Basic usage#

Popup is a StatelessWidget implementing CorePopupContract<Widget>. children is a required List<Widget>.

Popup(
  children: const [
    Text('Profile'),
    Text('Settings'),
    Text('Sign out'),
  ],
)

Default panel: surface background, 1px outline border (falls back to outlineVariant), radius 8, CoreShadow.md elevation, 4px padding all around, min-width 128 (all length values multiplied by the theme's scaling).

Direction#

direction is CoreAxis (default CoreAxis.vertical). Vertical lays children in a Column (mainAxisSize: min, crossAxisAlignment: stretch — entries fill the panel width); horizontal lays them in a min-size Row.

Popup(
  direction: CoreAxis.horizontal,
  children: const [
    Icon(LucideIcons.copy),
    Icon(LucideIcons.scissors),
    Icon(LucideIcons.clipboard),
  ],
)

Enum shorthand is idiomatic: direction: .horizontal.

Show / hide (open)#

open defaults to true. When false the widget renders SizedBox.shrink() — nothing is painted and no chrome is resolved.

Popup(
  open: isMenuOpen,
  children: menuEntries,
)

Per-instance style (popupStyle)#

popupStyle takes a CorePopupStyle. Fields (all optional): padding (CoreEdgeInsets?), borderRadius (CoreBorderRadius?), borderWidth (double?), backgroundColor / borderColor / shadowBaseColor (CoreColor?), boxShadow (List<CoreShadowLayer>?), minWidth (double?). It also exposes merge, copyWith, and the design-system defaults as statics (defaultPadding, defaultBorderRadius, defaultBorderWidth, defaultBackgroundColor, defaultBorderColor, defaultBoxShadow, defaultShadowBaseColor, defaultMinWidth).

Popup(
  popupStyle: const CorePopupStyle(
    minWidth: 240,
    padding: CoreEdgeInsets.all(CoreSpace.space8),
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    boxShadow: CoreShadow.lg,
  ),
  children: const [
    Text('Wide entry'),
  ],
)

Render structure (Flutter)#

ConstrainedBox(minWidth)DecoratedBox (background + border + radius + shadow) → ClipRRect (children clipped to the panel radius) → PaddingColumn/Row. There is no internal scroll view — wrap long content yourself if needed.

Patterns#

Button's menu variant with expanded: true gives full-width menu-styled rows inside the stretched vertical panel (this is how the higher-level menu components compose their entries):

Popup(
  children: [
    Button(
      variant: CoreButtonVariant.menu,
      expanded: true,
      onPressed: () => _openProfile(),
      child: const Text('Profile'),
    ),
    Button(
      variant: CoreButtonVariant.menu,
      expanded: true,
      onPressed: () => _signOut(),
      child: const Text('Sign out'),
    ),
  ],
)

Anchored overlay (positioning is the caller's job)

Popup paints chrome only — pair it with overlay machinery; the panel is what goes inside the overlay entry:

OverlayPortal(
  controller: _portalController,
  overlayChildBuilder: (context) => Positioned(
    top: _anchor.bottom,
    left: _anchor.left,
    child: Popup(
      children: const [
        Text('Rename'),
        Text('Delete'),
      ],
    ),
  ),
  child: Button(
    onPressed: _portalController.toggle,
    child: const Text('Actions'),
  ),
)

For standard cases prefer the higher-level CoUI menu components — they already anchor a Popup and expose the same popupStyle slot for the panel.

Web (coui_web / Jaspr)#

Import#

import 'package:coui_web/coui_web.dart';

Single barrel only — it re-exports coui_core and Jaspr's component/DOM API.

Basic usage#

The Web Popup is a UiComponent implementing CorePopupContract<Component> — same parameter names and semantics as Flutter (the source states full 1:1 parity); children is a List<Component>.

Popup(
  children: [
    div([text('Profile')]),
    div([text('Settings')]),
  ],
)

It renders a <div role="menu"> with flex flex-col (or flex-row for horizontal) + overflow-hidden classes; panel chrome (border, radius, shadow, padding, min-width in rem) goes inline, and token-bound colors emit Tailwind classes (dark-mode reactive) while raw color overrides go inline. When open is false it renders an empty fragment.

Popup(
  direction: CoreAxis.horizontal,
  open: isOpen,
  children: toolbarEntries,
)

Web-only constructor extras#

The Web constructor additionally accepts id, classes, css (Jaspr Styles), attributes, and eventHandlers. User classes are appended after the resolved class string; user css is combined after the resolved inline styles. The component also provides copyWith(...).

Popup(
  classes: 'z-50',
  children: [
    div([text('Entry')]),
  ],
)

popupStyle (shared style)#

Web reuses the exact same CorePopupStyle from coui_core:

Popup(
  popupStyle: const CorePopupStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius4),
    minWidth: 192,
  ),
  children: [
    div([text('Custom')]),
  ],
)

Parameters#

ParameterType (Flutter)Type (Web)DefaultNotes
children List<Widget> List<Component> required Panel content (entries, dividers, …)
open bool bool true false → renders nothing
direction CoreAxis CoreAxis CoreAxis.vertical vertical = Column/flex-col, horizontal = Row/flex-row
popupStyle CorePopupStyle? CorePopupStyle? null Per-instance panel chrome

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

Semantic axis + default token table#

Popup has no variant and no size (CoreComponentSize does not apply). The full set of its one semantic enum:

CoreAxis (full set)Layout
vertical (default) Children stacked top-to-bottom, stretched to panel width
horizontalChildren in a row, hugging content

Design-system defaults (single source of truth: CorePopupStyle.default* statics in coui_core, shared by both platform resolvers):

Style fieldDefault tokenValue
padding CoreEdgeInsets.all(CoreSpace.space4) 4 all sides
borderRadius CoreBorderRadius.all(CoreRadius.radius8) 8
borderWidthCoreStrokeWidth.stroke11
backgroundColorCoreColors.surfacetheme surface
borderColor CoreColors.outline (fallback outlineVariant) theme outline
boxShadowCoreShadow.mdmedium elevation
shadowBaseColor CoreColors.onSurface shadow tint base
minWidthCoreSpace.space128128

Project-level theming#

CorePopupTheme has exactly one slot — style (there is no variantStyles; popup has no variants). Both platform resolvers read theme.coreComponentTheme?.popup. Resolve chain:

CorePopupStyle.default*                 // design-system defaultCorePopupTheme.style                // 프로젝트 공통
  → widget.popupStyle                   // 인스턴스별
const CorePopupTheme(
  style: CorePopupStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    boxShadow: CoreShadow.lg,
  ),
)

Because dependent panel components (dropdown menu, menubar, navigation menu) derive chrome from the popup's fully-resolved style, a project-level popup theme override propagates consistently to them.

Flutter ↔ Web differences#

Aspect Flutter (coui_flutter) Web (coui_web / Jaspr)
Class Popup (StatelessWidget, CorePopupContract<Widget>) Popup (UiComponent, CorePopupContract<Component>)
children List<Widget> (e.g. Text('x')) List<Component> (e.g. div([text('x')]))
Closed (open: false) SizedBox.shrink() empty fragment
Render ConstrainedBoxDecoratedBoxClipRRectPaddingColumn / Row <div role="menu"> with flex classes + inline chrome
Scaling borderWidth / padding / minWidth multiplied by theme scaling emitted in rem (border-width, min-width)
A11y none built-in (panel only) role="menu" attribute on the root div
Extra ctor params id , classes , css , attributes , eventHandlers ; also copyWith(...)

Shared on both: parameter names/semantics (children, open, direction, popupStyle), CorePopupStyle (identical fields), CorePopupTheme, CoreAxis, and the resolve chain. No callbacks exist on either platform — Popup is a passive surface.

Pitfalls#

  • Popup is chrome-only — it does not anchor, position, or dismiss itself; overlay placement and outside-tap-to-close belong to the consuming component (dropdown menu, context menu, menubar, navigation menu).
  • There is no internal scroll view in either render — long content must be wrapped in a scrollable container by the caller.
  • Popup exposes no callbacks on either platform (no onClose, no anchor/offset props) — it is a purely passive surface, unlike a self-positioning legacy popup.
  • CorePopupTheme has exactly one slot, style — there is no variantStyles, since Popup has no variant axis to theme per-variant.
  • On Web, user-supplied classes are appended after the resolved class string and user css is combined after the resolved inline styles — they layer on top rather than replace resolved chrome.

v0.127 Migration (Legacy)#

v0.127 is a de-prefix hard break plus a cross-platform unification for this component: the panel container is now bare Popup on both platforms, with no Co* alias.

❌ Legacy (pre-0.127, removed)✅ v0.127
CoPopup(children: …)Popup(children: …)
MenuPopup (legacy Flutter menu panel, per the source doc) Popup — same parameter names, same semantics, full visual parity

Unchanged (already canonical, keep as-is):

  • Core* contract types: CorePopupStyle, CorePopupTheme, CorePopupContract, CoreAxis, CoreEdgeInsets, CoreBorderRadius, CoreColor, CoreShadow, …
  • The single-barrel imports: package:coui_flutter/coui_flutter.dart / package:coui_web/coui_web.dart.

Additional migration notes:

  • ❌ Do not look for variant:/size: on the popup — they never existed in the v0.127 contract; older menu-panel docs that showed size parameters do not apply.
  • ❌ Do not expect positioning/dismiss props (anchor, offset, onClose) on Popup — the legacy pattern of a self-positioning popup is gone; anchoring lives in the consuming overlay components.