Repository navigation
UI Overlays
Floating UI: popovers, menus, dialogs and windows. All are teleported to <body> and marked with
data-zen-layer (see UI components). Part of
@nynxz/zenkit-ui.
| Need | Use |
|---|---|
| Menu or small panel anchored to a button | ZenPopover + ZenMenuItem |
| Right-click menu at the pointer | ZenContextMenu |
| Blocking dialog | ZenModal |
| Movable, resizable tool window | ZenWindow |
| Dockable panel that survives clicking back on the graph | A ZenKit panel (Panels and workspaces, or openZenPanel in nodekit) |
One surface. Every ZenKit pop-up — ZenPopover, ZenContextMenu, and the ZenSelect, ZenCombo, ZenMentionInput and ZenColorPicker lists — shares one look from lib/surface.css (class="zen-surface"): --zen-chrome-bg, --zen-surface-border, --zen-radius-surface, and the shadow --zen-shadow-pop (default 0 10px 30px rgb(0 0 0 / 45%)), in ComfyUI's font. Give a custom pop-up the same class to match.
An anchored floating panel that flips to stay on screen and re-places on scroll and resize. Use it
uncontrolled with a trigger slot, or controlled with v-model:open and an anchor.
<ZenPopover placement="bottom-end">
<template #trigger="{ toggle }">
<ZenIconButton icon="mdi mdi-dots-vertical" title="More" @click="toggle" />
</template>
<template #default="{ close }">
<ZenMenuItem icon="mdi mdi-pencil" @select="rename(); close()">Rename</ZenMenuItem>
<ZenMenuSeparator />
<ZenMenuItem icon="mdi mdi-delete" danger @select="remove(); close()">Delete</ZenMenuItem>
</template>
</ZenPopover>Controlled, anchored to a point:
<ZenPopover v-model:open="menuOpen" :anchor="{ x: e.clientX, y: e.clientY }">…</ZenPopover>| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean | undefined |
undefined |
Bind with v-model:open to control it; leave unbound for trigger-slot use |
anchor |
HTMLElement | DOMRect | { x: number; y: number } |
the trigger | What to position against |
placement |
'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'right-start' | 'left-start' |
'bottom-start' |
|
offset |
number |
6 |
px gap from the anchor |
matchWidth |
boolean |
false |
Minimum width = anchor width |
role |
'menu' | 'dialog' | 'listbox' | 'tooltip' |
'menu' |
Its ARIA role: dialog for a form or picker, listbox for a choice of values |
| Event | Payload |
|---|---|
update:open |
boolean |
| Slot | Props | Description |
|---|---|---|
trigger |
{ toggle, open, close, active } |
The element that opens it |
| default | { close } |
Popover content |
Exposed: open(), close(), toggle().
An action row inside a popover. With a submenu slot it becomes a flyout trigger that opens on
hover.
<ZenMenuItem icon="mdi mdi-export">
Export
<template #submenu>
<ZenMenuItem @select="exportPng">PNG</ZenMenuItem>
<ZenMenuItem @select="exportJson">JSON</ZenMenuItem>
</template>
</ZenMenuItem>
<ZenMenuItem icon="mdi mdi-content-copy" @select="copy">Copy<template #hint>Ctrl+C</template></ZenMenuItem>| Prop | Type | Default | Description |
|---|---|---|---|
icon |
string |
— | Full MDI class |
danger |
boolean |
false |
Red text |
disabled |
boolean |
false |
| Event | Description |
|---|---|
select |
Clicked (not emitted when disabled or for submenu triggers) |
| Slot | Description |
|---|---|
| default | Label |
lead |
Before the icon — a ZenDot for a toggle row |
hint |
Right-aligned hint (plain items) |
submenu |
Flyout content |
A hairline divider between menu items. No props, events or slots.
<ZenMenuSeparator />A right-click menu at the pointer, driven imperatively through a template ref.
<script setup lang="ts">
import { ref } from 'vue'
import { ZenContextMenu, type ContextMenuItem } from '@nynxz/zenkit-ui'
const menu = ref<InstanceType<typeof ZenContextMenu> | null>(null)
const items: ContextMenuItem[] = [
{ heading: 'Clip' },
{ label: 'Rename', icon: 'mdi-pencil', hint: 'F2', run: rename },
'-',
{ label: 'Delete', icon: 'mdi-delete', danger: true, run: remove },
]
</script>
<template>
<div @contextmenu.prevent="menu?.show($event)">…</div>
<ZenContextMenu ref="menu" :items="items" />
</template>| Prop | Type | Default | Description |
|---|---|---|---|
items |
ContextMenuItem[] |
required | Actions, headings and '-' separators |
ContextMenuItem = ContextMenuAction | { heading: string } | '-'
ContextMenuAction:
| Field | Type | Description |
|---|---|---|
label |
string |
|
icon |
string? |
MDI glyph ('mdi-pencil') |
hint |
string? |
Right-aligned |
danger |
boolean? |
|
disabled |
boolean? |
|
keepOpen |
boolean? |
Stay open after run, for toggles |
run |
() => void |
Called on click, after the menu has closed |
Exposed: show(e: MouseEvent, opts?: { clear?: DOMRect }), close(). No events or slots. The menu
opens at the pointer and stays at least 6px inside the window. clear is a strip it must not cover
(a taskbar's rect): it opens above the strip, or below it when there is no room above.
A centred dialog with a backdrop. Pressing the backdrop or Escape closes it.
<ZenModal v-model:open="showSettings" title="Settings" width="560px">
<ZenField label="Name"><ZenInput v-model="name" /></ZenField>
<template #footer>
<ZenButton @click="showSettings = false">Cancel</ZenButton>
<ZenButton variant="primary" @click="save">Save</ZenButton>
</template>
</ZenModal>| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
required | Use v-model:open
|
title |
string |
— | |
width |
string |
'720px' |
Any CSS length |
height |
string |
'70vh' |
Any CSS length |
| Event | Payload |
|---|---|
update:open |
boolean (only ever false) |
| Slot | Description |
|---|---|
header |
Between the title and the close button |
| default | Body (14px padding, scrolls) |
footer |
A draggable, resizable, maximisable window. Modeless unless backdrop is set.
<ZenWindow
v-model:open="editing"
title="Mask editor"
icon="mdi mdi-brush"
:width="900"
:height="640"
>
<template #actions><ZenIconButton icon="mdi mdi-undo" title="Undo" @click="undo" /></template>
<MaskEditor />
</ZenWindow>| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
required | Use v-model:open
|
title |
string |
'' |
|
icon |
string |
'mdi mdi-application-outline' |
|
width, height
|
number |
0 |
Initial size in px; 0 = 70% / 82% of the viewport |
minWidth |
number |
420 |
|
minHeight |
number |
320 |
|
backdrop |
boolean |
false |
Dim and block what's behind |
closeOnEsc |
boolean |
true |
Ignored while typing in a field |
| Event | Payload |
|---|---|
update:open |
boolean (false) |
maximize |
boolean |
| Slot | Description |
|---|---|
actions |
Header buttons |
| default | Body (display: flex) |
footer |
Getting started
Guides
- Panels and workspaces
- Apps and routing
- Themes
- Backgrounds
- Channels and media
- Jobs
- Capabilities
- Storage and bus
Node packs
UI components
Reference