CoUI Popup#
ํ๋ง๋๋ก (๋น๊ฐ๋ฐ์์ฉ)#
ํ์ (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.
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.