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, nosize. Popup carries no semantic variant/size enums — its only semantic parameter isdirection: CoreAxis(verticaldefault,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) → Padding → Column/Row. There is no internal scroll view — wrap long content yourself if needed.
Patterns#
Menu panel with button rows
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#
| Parameter | Type (Flutter) | Type (Web) | Default | Notes |
|---|---|---|---|---|
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 |
horizontal | Children in a row, hugging content |
Design-system defaults (single source of truth: CorePopupStyle.default* statics in coui_core, shared by both platform resolvers):
| Style field | Default token | Value |
|---|---|---|
padding |
CoreEdgeInsets.all(CoreSpace.space4) |
4 all sides |
borderRadius |
CoreBorderRadius.all(CoreRadius.radius8) |
8 |
borderWidth | CoreStrokeWidth.stroke1 | 1 |
backgroundColor | CoreColors.surface | theme surface |
borderColor |
CoreColors.outline (fallback outlineVariant) |
theme outline |
boxShadow | CoreShadow.md | medium elevation |
shadowBaseColor |
CoreColors.onSurface |
shadow tint base |
minWidth | CoreSpace.space128 | 128 |
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 default
→ CorePopupTheme.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 |
ConstrainedBox
→
DecoratedBox
→
ClipRRect
→
Padding
→
Column
/
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. -
CorePopupThemehas exactly one slot,style— there is novariantStyles, since Popup has no variant axis to theme per-variant. -
On Web, user-supplied
classesare appended after the resolved class string and usercssis 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.