WovenUI started as an SCSS framework: readable utility classes, an apply() mixin that takes any value, and design tokens for light, dark and high-contrast themes. Core is the foundation layer. It styles plain buttons and form fields, and it stops there.
@wovenui/components is the layer on top: buttons, cards, forms, tables, modals, drawers, dropdowns, tabs, toasts and more, written as plain markup with wu- classes. It needs one companion package, @wovenui/core, which provides the tokens and themes the components read.
This post covers how the kit is built, and one lesson about dialogs that applies to any app, whatever UI kit it uses.
One class on native HTML
The kit leans on what the browser already does. Modals and drawers are <dialog> elements. Dropdowns and popovers use the popover attribute. Accordions are <details> elements, and accordions that share a name keep only one open. Tooltips come from core, through data-tooltip.
Here is a modal, as it appears in the component's own documentation:
<button class="wu-btn" type="button" commandfor="m1" command="show-modal">Open</button>
<dialog class="wu-modal" id="m1" aria-labelledby="m1-title" closedby="closerequest">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="m1-title">Title</h2>
<button class="wu-modal-close" type="button" commandfor="m1" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">Content</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn">Cancel</button>
<button class="wu-btn brand" value="confirm">Save</button>
</form>
</dialog>
There is no script on that page. The commandfor attribute on a button names the element it controls, and command says what to do (show-modal, close). These are invoker commands. The browser opens and closes the dialog, moves focus and handles the Esc key. showModal() and the command attribute lock page scroll. The body scrolls on its own when the content is taller than the dialog.
closedby decides how the dialog can be dismissed. closedby="any" adds click outside, which suits read-only content. A dialog that holds a form takes closedby="closerequest": a backdrop click with a menu open would otherwise close both and lose the input.
A drawer is the same idea, docked to the inline end by default, with inline-start or bottom to move it. Right-to-left pages mirror the slide.
Menus without a script
A dropdown is a native popover, anchored under its trigger:
<button class="wu-btn caret" type="button" popovertarget="menu1">Actions</button>
<div class="wu-dropdown" id="menu1" popover>
<a class="wu-dropdown-item" href="/edit">Edit</a>
<button class="wu-dropdown-item" type="button" popovertarget="menu1" popovertargetaction="hide">Duplicate</button>
<hr class="wu-dropdown-separator">
<button class="wu-dropdown-item danger" type="button" popovertarget="menu1" popovertargetaction="hide">Delete</button>
</div>
Esc and a click outside close it. popovertargetaction="hide" on a button item closes the menu after its click handler runs. commandfor with command="toggle-popover" works as the trigger too.
Placement uses CSS anchor positioning. The menu opens under the trigger and flips when there is no room. Where anchor positioning is not available, it is centered in the viewport. The inline-end modifier aligns the menu's inline end with the trigger's, as for the caret of a split button. The popover component works the same way, with a title part and an actions row for an inline confirm.
Where a script is still needed
Most components need no JavaScript. Modals, drawers, dropdowns, popovers and accordions need none. A small optional script, with no dependencies, covers the few that do: tabs, toasts, autocomplete, data table sorting, table row selection and expandable rows, and the input clear button.
The script finds these components by their class and starts them when the page loads. There is no setup attribute. As an ES module it exports init, toast, dismiss and per-component starters:
import { init, toast } from '@wovenui/components/js';
toast('Your changes are live.', { title: 'Saved', variant: 'success' });
Apps built with React, Vue or another framework can skip the script and keep the CSS. State lives in attributes (aria-selected, aria-expanded, aria-sort, hidden), not classes, so a framework sets the same attributes from component state.
Tokens, themes and layers from core
The components read core's design tokens, so they change with the theme. Change colors, spacing, radii or type once in core, and the components follow in light, dark, high-contrast and inverse sections.
Dark-only details use core's dark-mode mixin, which emits CSS @scope. The modal backdrop is one example: 50% black by default, 70% in dark themes, and it applies exactly where the dark tokens resolve, including a dark section inside a light page.
Every component lives in @layer components. The CSS declares the order @layer reset, base, components, recipes;. Core uses the first three; recipes is an extra layer above the components for patterns you build from them, and the package puts nothing in it. Utility classes and your own unlayered CSS win over the components without !important.
Naming stays small. The root carries the prefix, modifiers are bare words, and parts use the root name plus a part:
<button class="wu-btn" type="button">Cancel</button>
<button class="wu-btn brand" type="button">Save</button>
<button class="wu-btn danger subtle sm" type="button">Delete</button>
Modifiers are emitted as compounds (.wu-btn.brand), never alone, so they do not clash with other class names. Color words are core role names: brand, success, danger, warning, info.
Two more defaults: components use logical properties, so they follow dir="rtl" from the same stylesheet with no separate build. And motion is off until you ask for it with the word animated on a component, or wu-animated on an ancestor; it never runs when the visitor has asked the system to reduce motion.
The top layer lesson
Once modals and drawers are real dialog elements, they open in the browser's top layer. That changes the rules for everything that floats. A menu, tooltip or autocomplete list positioned with z-index in the normal page renders behind the dialog, whatever the number. To appear above a modal, it has to be in the top layer too.
The kit's dropdowns and popovers already are, because they are popovers. There is one more rule, which the modal docs state directly: a menu, popover or toast used while a modal is open must be inside the dialog element. A modal dialog makes the rest of the page inert, so anything outside it cannot be clicked. The kit's toast() handles this for toasts: while a modal dialog is open, it moves the toast region into the topmost one, so its toasts can be clicked and are announced, and moves it back when that dialog closes.
We hit this moving an existing Ember app onto the kit. The app had its own tooltip and autocomplete services, each rendering into a portaled container at the end of the body with a high z-index. Once the app's modals became native dialogs, those tooltips and suggestion lists opened behind them.
The fix is small. Give the portaled container popover="manual", call showPopover() when it has content, and hidePopover() when it is empty. A manual popover has no light dismiss, so the service keeps full control over when it closes, and once shown it sits in the top layer above the dialog that opened before it. For an interactive container such as an autocomplete list, the rule above still applies: place it inside the open dialog, as toast() does with its region, so the options stay clickable.
Install
npm i @wovenui/core @wovenui/components
Load core first, then the components:
<link rel="stylesheet" href="node_modules/@wovenui/core/dist/wovenui.min.css">
<link rel="stylesheet" href="node_modules/@wovenui/components/dist/wovenui-kit.min.css">
With Sass and a bundler, load both in your global stylesheet:
@use "@wovenui/core";
@use "@wovenui/components";
Every component is on by default. Turn some off with $components, or load one component at a time (@use "@wovenui/components/button";) for CSS modules and component <style> blocks. Pick one path per project, not both, or shared rules are emitted twice.
The kit works in Chrome and Edge 135+, Firefox 144+ and Safari 26.2+.
The package also ships dist/llms-full.txt, an AI guide with every component, its classes, modifiers, variables and examples. Give it to an AI coding assistant so it writes correct markup.
Working with an AI agent
The kit ships a full reference for AI agents. Point your coding agent at it and it writes correct kit markup and SCSS without guessing: every component, its classes, modifiers, variables and examples. Core has its own file for the framework, and the components package has a separate one for the kit. The kit guide is at wovenui.com/components/llms-full.txt and the core guide is at wovenui.com/llms-full.txt.
What is next
The kit and core moved together: the last two kit releases each required a newer core. The next post walks through what changed from core 2.0 to 2.4, and which of those changes the components asked for.
Links
- Documentation: wovenui.com/components
- GitHub: github.com/WovenUI/components
- npm: @wovenui/components
- Core: wovenui.com
WovenUI Components is MIT licensed.
Top comments (0)