@monowind/ui-vue

@monowind/ui as Vue composables: a menu, a listbox, a select, a combobox, a dialog, a popover, a tooltip, a collapsible, an accordion on the monowind grid
885 4
Install
npm install @monowind/ui-vue
Latest Version:0.3.5
Vue:3
License:MIT
Last Updated:Oct 6, 2026
Links: GitHub  ·  npm
Maintainer: benface.com

@monowind/ui-vue

@monowind/ui for Vue, two ways over the same machines: a component per part — MenuRoot, MenuTrigger, DialogRoot, … — and a composable per component — useMenu, useListbox, useSelect, useCombobox, useDialog, usePopover, useTooltip, useCollapsible, useAccordion. Style the parts with Tailwind and monowind classes; the engine places each floating part against its trigger in cells, in the top layer.

<script setup lang="ts">
import { MenuContent, MenuItem, MenuPositioner, MenuRoot, MenuTrigger } from "@monowind/ui-vue";

const item = "px-1 data-highlighted:bg-(--mw-fg) data-highlighted:text-(--mw-bg)";
</script>

<template>
  <MenuRoot
    :positioning="{ placement: 'bottom-start' }"
    @select="({ value }) => console.log(value)"
  >
    <MenuTrigger class="border px-1">File</MenuTrigger>
    <MenuPositioner>
      <MenuContent class="border bg-clear">
        <MenuItem value="new" :class="item">New</MenuItem>
      </MenuContent>
    </MenuPositioner>
  </MenuRoot>
</template>

The components

…Root takes the machine's props — id optional, a useId() standing in — and provides the API to the parts under it. A callback is a listener, so onSelect is @select and onOpenChange is @open-change. The parts:

  • Menu — MenuTrigger, MenuTriggerItem, MenuPositioner, MenuContent, MenuItem, MenuItemGroup, MenuItemGroupLabel, MenuSeparator.
  • Dialog and Popover — Trigger, Positioner, Content, Title, Description, CloseTrigger under each name; the popover also has PopoverIndicator.
  • Tooltip — TooltipTrigger, TooltipPositioner, TooltipContent, which render inline, a tooltip belonging in the sentence around its trigger.
  • Listbox and Select — Root, which is an element of its own (Zag gives both a root part, so it takes attributes as any part does), Label, Content, Item, ItemText, ItemIndicator, ItemGroup, ItemGroupLabel under each name; the select adds SelectControl, SelectTrigger, SelectValueText, SelectIndicator, SelectClearTrigger, SelectPositioner, SelectList and SelectHiddenSelect, the native control a form submits — its options in the server's render too, so a form sent before hydration posts the initial value.
  • Combobox — ComboboxRoot (an element of its own), ComboboxLabel, ComboboxControl, ComboboxInput, ComboboxTrigger, ComboboxClearTrigger, ComboboxPositioner, ComboboxContent, ComboboxList, and the item parts above. Its list anchors to the control, so it lines up under the input; filtering is yours, and the items your collection leaves out are hidden.
  • Collapsible — CollapsibleRoot (an element of its own), CollapsibleTrigger, CollapsibleContent, CollapsibleIndicator.
  • Accordion — AccordionRoot (an element of its own), AccordionItem, AccordionItemTrigger, AccordionItemContent, AccordionItemIndicator; an item holds its value and disabled for the parts inside, which useAccordionItemContext() reads.

A root emits update:open, update:value, update:highlighted-value and update:trigger-value where its machine has them, so v-model works as anywhere else:

<SelectRoot v-model:open="open" v-model:value="value" :collection="items">

A menu's, a dialog's, a popover's and a tooltip's root renders nothing — Zag gives those four no root part, and a wrapper invented for one would put a box in the grid's layout — so an attribute on one goes nowhere, and a development build warns and names it. A listbox's, a select's, a combobox's, a collapsible's and an accordion's root renders its part and takes attributes as any part does.

An item names one of the collection's items, by item or by the value that finds it there, and holds it for the text and the indicator inside; useListboxItemContext(), useSelectItemContext() and useComboboxItemContext() read it. The item parts read the nearest listbox, select or combobox root, whichever of the three they are named for. SelectValueText shows what is selected, its slot the placeholder until something is. A MenuRoot inside another is that menu's submenu: Zag links the two, and the submenu takes the side it opens on (right-start, or left-start in a right-to-left menu) and the behavior its parent shares — onSelect, closeOnSelect, loopFocus, typeahead, composite, navigate, dir, getRootNode — with its own props over them, exactly as a marked submenu root does. Its MenuTriggerItem is the parent's item that opens it, so it goes inside the nested MenuRoot.

Every part merges the API's props under yours with Zag's own mergeProps, so a handler of yours runs before the API's. as-child renders the one element the default slot gives instead of the part's own, with those props on it — refs from both sides reach the node:

<MenuTrigger as-child>
  <MyButton class="border px-1">File</MyButton>
</MenuTrigger>

…RootProvider takes an API you hold, for reaching it from outside the tree: run the composable yourself and hand it over on value. A context composable per component — useMenuContext(), useAccordionContext() and the rest — gives a part the component it is in, exactly as its composable returns it.

The composables

Each takes Zag's machine props (id required) — or a ref or getter of them, so a controlled open or highlightedValue follows your state — and returns api, Zag's API with the grid's props on the trigger, the positioner, and the content, as a computed; positioner, the ref for the positioner element that keeps its place in the top layer with the machine; and service, the machine's, for linking two. A listbox, a collapsible and an accordion stand in the flow, so useListbox, useCollapsible and useAccordion return no positioner.

<script setup lang="ts">
import { useMenu } from "@monowind/ui-vue";

const { api, positioner } = useMenu({ id: "file", positioning: { placement: "bottom-start" } });
</script>

<template>
  <button v-bind="api.getTriggerProps()" class="border px-1">File</button>
  <div ref="positioner" v-bind="api.getPositionerProps()">
    <div v-bind="api.getContentProps()" class="border bg-clear">
      <div
        v-bind="api.getItemProps({ value: 'new' })"
        class="px-1 data-highlighted:bg-(--mw-fg) data-highlighted:text-(--mw-bg)"
      >
        New
      </div>
    </div>
  </div>
</template>

Tell Vue's compiler that mono-wind is a custom element (isCustomElement: (tag) => tag === "mono-wind" in the Vue plugin's template.compilerOptions). The parts, their states, and positioning (placement, gutter, offset, in cells) are @monowind/ui's — see its README.

Related Packages

vue-aria-focus-trap

A Vue.js component that implements WAI-ARIA focus trapping

2,492 0
@typeach/core

An unstyled component library for Vue.

3,364
vue-accessible-date-field

An open source Vue date field component designed to be as accessible as possible...

1,092
vue-accessible-modal

Vue.js component for accessible modals

6,117 21
chusho

A library of bare accessible Vue components and tools.

2,193 55

Version History

Version Released Vue License
0.3.5 >=3.5.0 MIT
0.3.4 >=3.5.0 MIT
0.3.3 >=3.5.0 MIT
0.3.2 >=3.5.0 MIT
0.3.1 >=3.5.0 MIT