11# Spec: ` @monowind/ui ` — accessible components on the grid
22
33Status: ** implemented** (2026-09-13, the framework packages and
4- examples 2026-09-18; ` packages/ui ` , one entry per component, plan
5- ` 2026-09-13-ui.md ` ). The engine features it needs are
6- ` top-layer.md ` and ` anchor-positioning.md ` ; motion is ` animations.md ` .
4+ examples 2026-09-18, the listbox and the select 2026-09-21;
5+ ` packages/ui ` , one entry per component, plan ` 2026-09-13-ui.md ` ). The
6+ engine features it needs are ` top-layer.md ` and
7+ ` anchor-positioning.md ` ; motion is ` animations.md ` , and a list's scroll
8+ is ` scrolling.md ` .
79
810## Motivation
911
10- A menu, a dialog, a popover, a tooltip: every application needs them,
11- and doing them accessibly — roles and states, roving focus, typeahead,
12+ A menu, a list to choose from, a dialog, a popover, a tooltip: every
13+ application needs them, and doing them accessibly — roles and states,
14+ roving focus, typeahead,
1215focus trapping and restore, dismissal, nested menus — is where most
1316hand-rolled components fail. monowind is a custom element that renders
1417whatever DOM it holds, so its components must be framework-agnostic
@@ -21,9 +24,10 @@ the engine places and layers the parts, Zag runs them.
2124## Shape
2225
2326- ** One package, a subpath per component** : ` @monowind/ui/menu ` ,
24- ` @monowind/ui/dialog ` , ` @monowind/ui/popover ` , ` @monowind/ui/tooltip `
25- first; ` select ` , ` combobox ` , ` tabs ` and the rest follow the same
26- shape. Versioned in lockstep with ` monowind ` .
27+ ` @monowind/ui/listbox ` , ` @monowind/ui/select ` , ` @monowind/ui/dialog ` ,
28+ ` @monowind/ui/popover ` , and ` @monowind/ui/tooltip ` , in that order
29+ wherever they are listed; ` combobox ` , ` tabs ` and the rest follow the
30+ same shape. Versioned in lockstep with ` monowind ` .
2731- ** A core that touches no DOM** : per component, ` props(p) ` , the
2832 machine's props with Zag's own positioning off, Zag's ` machine `
2933 re-exported, ` connect(service, normalizeProps, p) ` — Zag's connect
@@ -77,6 +81,53 @@ props)` is the core plus `VanillaMachine` and `spreadProps`: parts
7781 highlight, and ids are its own. It returns the API and a ` destroy `
7882 that stops the machine and takes its handlers off the parts, after
7983 which a mount on the same markup wires it again.
84+ - ** A component with no floating part stands in the flow.** A listbox
85+ is roles, selection, and keyboard alone, so the engine lays its parts
86+ out as it lays out any box and the entry adds no props to Zag's. It
87+ takes the mount the anchored components build on, without the
88+ trigger, positioner, and content that one spreads, and the framework
89+ packages return its API alone. Its scroll is the engine's
90+ (scrolling.md): ` overflow-y-auto ` with a ` max-h-* ` on the content
91+ scrolls natively, a select's list the same, and Zag scrolling the
92+ highlighted item into view repaints the grid on the cells it moved
93+ to. That scroll targets the content box, not the scrollport
94+ (` scrollToIndexFn ` in the machine's props, an author's own over it):
95+ the engine reserves a box's border cells as padding, so a scroll into
96+ the scrollport would stop with the item under the border.
97+ - ** A list's focus starts at its selection.** The content taking focus
98+ highlights the selected item, or the first where nothing is selected,
99+ as ARIA's listbox pattern has it, and brings that row into view. A
100+ composite's own cells stay plain and the grid draws no focus ring to
101+ fall back on (cell-model.md), so the highlight is the whole of the
102+ indication and a stale one misplaces it: Zag leaves the highlight
103+ empty on focus wherever a value is selected, and a pointer passing
104+ over an unfocused list leaves its own, which nothing shows until the
105+ next focus. The scroll skips a focus the pointer caused, as the
106+ machine's own does, so a press on a visible item never jumps the list
107+ out from under it.
108+ - ** The markup is a listbox's collection.** Zag takes its items as a
109+ ` collection ` the author builds; for markup that names none, the
110+ marked items make one — the ` data-value ` each carries, the words of
111+ its ` item-text ` , and ` data-disabled ` — so a list of static items is
112+ markup alone, and an item the author's own collection leaves out
113+ stays plain markup. The mount's own element is the ` root ` , its id the
114+ markup's so the page still finds it, and ` data-highlight-on-hover ` on
115+ it passes Zag's ` highlightOnHover ` for every item: it moves the
116+ highlight, Zag keeping ` data-highlighted ` for the keyboard's focus
117+ and leaving the pointer's feedback to ` hover: ` .
118+ - ** A select is a listbox on a trigger.** Zag's select is the anchored
119+ one — the same collection, items, and keyboard, its content in the
120+ top layer — so it takes the anchored mount and the placement mapping
121+ unchanged, and shares the items and the scroll with the listbox. Two
122+ parts are the mount's to fill, a markup author having nothing else to
123+ render them with: the ` value-text ` , whose markup text is its
124+ placeholder and whose content the mount writes from the API; and an
125+ optional ` hidden-select ` , the native control a form posts under the
126+ machine's ` name ` , filled with an option per item and set to
127+ ` display: none ` , which the layout skips while a form still submits
128+ it; where the select takes several values, the options carry the
129+ selection, the adapter's one assignment of ` value ` being a single
130+ control's.
80131- ** States are attributes** : ` data-state ` , ` data-highlighted ` ,
81132 ` data-disabled ` , ` data-placement ` , as Zag sets them, so an author
82133 styles them with Tailwind's data variants
@@ -157,7 +208,8 @@ the functions; the functions change only where the elements need them
157208to, and additively.
158209
159210- ** Elements for markup** : ` <mono-menu> ` , ` <mono-submenu> ` ,
160- ` <mono-dialog> ` , ` <mono-popover> ` , ` <mono-tooltip> ` , from
211+ ` <mono-listbox> ` , ` <mono-select> ` , ` <mono-dialog> ` ,
212+ ` <mono-popover> ` , ` <mono-tooltip> ` , from
161213 ` @monowind/ui/elements ` , registered by ` defineMonoUi() ` as core's
162214 are by ` defineMonoWind() ` — the entry registers nothing on import,
163215 so the package's ` sideEffects: false ` holds; the CDN bundle calls it.
@@ -223,11 +275,15 @@ to, and additively.
223275false`), one namespace per component with Ark UI's anatomy —
224276 ` Menu.Root ` , ` Menu.Trigger ` , ` Menu.Positioner ` , ` Menu.Content ` ,
225277 ` Menu.Item ` , ` Menu.ItemGroup ` , ` Menu.ItemGroupLabel ` ,
226- ` Menu.Separator ` , ` Menu.TriggerItem ` ; ` Dialog.Root ` , ` .Trigger ` ,
278+ ` Menu.Separator ` , ` Menu.TriggerItem ` ; ` Listbox.Root ` , ` .Label ` ,
279+ ` .Content ` , ` .Item ` , ` .ItemText ` , ` .ItemIndicator ` , ` .ItemGroup ` ,
280+ ` .ItemGroupLabel ` ; ` Select ` the same with ` .Control ` , ` .Trigger ` ,
281+ ` .ValueText ` , ` .Indicator ` , ` .ClearTrigger ` , ` .Positioner ` , and
282+ ` .HiddenSelect ` around them; ` Dialog.Root ` , ` .Trigger ` ,
227283 ` .Positioner ` , ` .Content ` , ` .Title ` , ` .Description ` ,
228284 ` .CloseTrigger ` ; ` Popover ` the same; ` Tooltip.Root ` , ` .Trigger ` ,
229- ` .Positioner ` , ` .Content ` — Ark's other parts (arrows, indicators ,
230- a backdrop, checkbox and radio items) left for a later need. ` Root `
285+ ` .Positioner ` , ` .Content ` — Ark's other parts (arrows, a backdrop ,
286+ checkbox and radio items) left for a later need. ` Root `
231287 takes the machine's props, ` id ` optional and generated (` useId ` ,
232288 Vue's ` useId ` , Svelte's ` $props.id() ` ), renders nothing of its own,
233289 and hands the API down through context; ` RootProvider ` takes an
@@ -255,12 +311,20 @@ false`), one namespace per component with Ark UI's anatomy —
255311## Testing
256312
257313- Node: the placement mapping to ` position-area ` ; the vanilla path's
258- parts found and wired; the show and hide of the positioner around
259- the exit's transitions; the React hooks and Vue composables rendered
260- through their frameworks (` hooks.test.tsx ` , ` composables.test.ts ` ).
261- - Storybook, per component, in every engine: open and close by
262- pointer and keyboard; the floating part's cells directly under (or
263- beside, above) the trigger and the light element's box on them, over
314+ parts found and wired, a list's collection read from its markup and a
315+ select's hidden control among them; the show and hide of the
316+ positioner around the exit's transitions; the React hooks and Vue
317+ composables rendered through their frameworks (` hooks.test.tsx ` ,
318+ ` composables.test.ts ` ), a component with no floating part included.
319+ - Storybook, per component, in every engine: the listbox's roles, its
320+ value selected by press and by Enter with the indicator following it
321+ on the grid, the pointer highlight its root asks for, and the list
322+ scrolled to the item the keyboard highlights; the select's list
323+ anchored under its trigger, the value it puts on the trigger and in
324+ its form control, and that control left out of the layout; per
325+ anchored component, open and close by pointer and keyboard; the
326+ floating part's cells directly under (or beside, above) the trigger
327+ and the light element's box on them, over
264328 the page's later text; Escape and focus restore (an outside press
265329 dismisses through Zag's document listener on a deferred animation
266330 frame, which WebKit suspends in a backgrounded window, so the
@@ -297,7 +361,10 @@ false`), one namespace per component with Ark UI's anatomy —
297361- ` packages/ui ` : ` anchor.ts ` (the placement table, the anchor name,
298362 the props and their merge, ` anchoredApi ` ), ` top-layer.ts `
299363 (` syncTopLayer ` , its own ` ./top-layer ` subpath for the framework
300- packages), ` vanilla.ts ` (the parts, the mount), and ` menu.ts ` ,
364+ packages), ` vanilla.ts ` (the parts, the mount, and the anchored mount
365+ over it), ` items.ts ` (the markup's collection and the item parts a
366+ listbox and a select share), ` scroll.ts ` (the highlight scrolled into
367+ the content box), and ` menu.ts ` , ` listbox.ts ` , ` select.ts ` ,
301368 ` dialog.ts ` , ` popover.ts ` , ` tooltip.ts ` (each ` props ` , ` api ` , the
302369 mount); ` @zag-js/vanilla ` and the machines as dependencies;
303370 ` dist/cdn.js ` for classic scripts, ` monowind.ui ` .
0 commit comments