Documentation · Web Components
43 custom elements that use your stylesheet instead of their own.
The mue-* elements are plain ES modules with no shadow roots. They add markup and behaviour inside your document, so styles/components.css, the design tokens and the html.dark flip reach them without ::part or adoptedStyleSheets plumbing. One <script type="module"> registers every tag. Nothing is hosted, nothing is published to npm, and nothing needs a build.
Load them four ways
Pick the one that matches the project you already have. The first needs no tooling at all, which is the point of this port.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
--> <link rel=stylesheet href="design-tokens/engine.css">
<link rel=stylesheet href="styles/components.css">
<script src="styles/mue.js" defer></script>
--> <script type="module" src="frameworks/web-components/index.js"></script>
</head>
<body class="page">
<mue-button variant="primary">Save changes</mue-button>
<mue-input label="Email" type="email" name="email"
helper="Never shared with third parties."></mue-input>
<mue-card variant="elevated">
<div class="card-body">…</div>
</mue-card>
</body>
</html>
// every element, registered on import
import "./frameworks/web-components/index.js";
// or one element only, so the other 39 modules never reach the bundle
import "./frameworks/web-components/components/mue-dialog.js";
// loading index.js twice is harmless:
// every module guards with customElements.get(tag) before defining.
// the two stylesheets are still yours to import
import "./design-tokens/engine.css";
import "./styles/components.css";
$ node tools/mue.js add btn-buttons --framework web-components --into ../demo-app --create
added 1 web-components binding(s) into ../demo-app
btn-buttons — Buttons (stable)
frameworks/web-components/components/mue-button.js [written]
install: <script type="module" src="frameworks/web-components/index.js"></script>
import: import "./frameworks/web-components/index.js" (registers every mue-* tag)
tag: <mue-button>
verified: … (full line printed; quoted on the agent guide)
package files: 3 — 15 shared layer file(s) (15 written)
NOTES
· web-components/index.js was written with 39 module reference(s) left out —
those components are not in this install.
$ npm i file:./frameworks/web-components
$ node -e "import('mue-web-components')"
works, but reads the folder in place.
The file: install is the weakest of the four: it registers mue-web-components as a link to the folder inside the checkout rather than a copy, so deleting or moving the repository breaks the import. It is also not a published package — npm i mue-web-components will fail, because nothing has been published. The CLI page explains why this project installs files instead of resolving a registry.
What an element actually renders
Dumped from the browser after the modules registered, not written by hand. This is the contract: the host builds real child nodes out of the classes in styles/components.css, and the theme sheet sees them because they are in the document.
<mue-button variant="primary"><button type="button" class="btn-primary">Save changes</button></mue-button>
<mue-input label="Email" type="email" name="email" helper="…"><div class="field">
<label class="field-label" for="mue-input-1">Email</label>
<input id="mue-input-1" type="email" placeholder="" name="email">
<p class="field-helper" id="mue-input-1-helper">Never shared with third parties.</p>
</div></mue-input>
<mue-card variant="elevated" class="card card-elevated">…</mue-card>
The gallery, live in your browser
This is frameworks/web-components/preview/gallery.html from the repository, running in the frame below. Switch theme with the toggle in the header and the elements inside the frame repaint — they are reading the same document-level rules as the rest of the site.
The same elements are also listed in the searchable library; /preview?id=… opens any catalog entry with the source panel beside it.
Element reference
All 43 registered tags, read out of the 40 modules in frameworks/web-components/components/. Two modules ship more than one tag: mue-sidebar.js registers three and mue-guidance.js registers two. “slot children” means the element expects you to write its inner markup; the classes it looks for are in the gallery.
| Tag | Attributes read by the module | Emits | Module | Catalog entry |
|---|---|---|---|---|
| <mue-accordion> | — slot children | — | mue-accordion.js | ui-sections |
| <mue-accordion-item> | open, label | mue-change | mue-accordion-item.js | no catalog entry |
| <mue-avatar> | src, alt, initials, name | — | mue-avatar.js | no catalog entry |
| <mue-avatar-group> | label | — | mue-avatar-group.js | no catalog entry |
| <mue-badge> | variant, solid, dot | — | mue-badge.js | ui-descriptions |
| <mue-breadcrumbs> | items, separator, label | — | mue-breadcrumbs.js | nav-breadcrumbs |
| <mue-button> | variant, size, state, type, disabled, block, icon, aria-label | — | mue-button.js | btn-buttons |
| <mue-card> | variant, interactive, selected | — | mue-card.js | ui-cards |
| <mue-charts> | type, title, desc, values, labels, colors, legend, summary | — | mue-charts.js | ui-charts |
| <mue-checkbox> | label, name, value, checked, disabled | mue-change | mue-checkbox.js | frm-checkboxes |
| <mue-datatable> | — slot children | mue-select, mue-expand, mue-sort, mue-filter | mue-datatable.js | ui-datatable |
| <mue-date-picker> | label, type, name, value, min, max, step, placeholder, helper, message, state, disabled, required | mue-change | mue-date-picker.js | frm-date-picker |
| <mue-dialog> | open, title | mue-open, mue-close | mue-dialog.js | ui-dialogs |
| <mue-dropdown> | open | mue-select, mue-open, mue-close | mue-dropdown.js | ui-dropdowns |
| <mue-dropzone> | — slot children | mue-remove, mue-change | mue-dropzone.js | frm-dropzone |
| <mue-empty> | icon, title, desc, label | — | mue-empty.js | fb-empty |
| <mue-error> | title, desc, inline, label | — | mue-error.js | fb-error |
| <mue-fab> | static, reveal | mue-close, mue-step | mue-guidance.js | ui-guidance |
| <mue-footer> | brand, desc, copyright, columns, social, label | — | mue-footer.js | ui-footers |
| <mue-guidance> | current | mue-close, mue-step | mue-guidance.js | ui-guidance |
| <mue-header-nav> | brand | mue-navigate | mue-header-nav.js | nav-header |
| <mue-input> | label, type, name, value, placeholder, helper, message, state, disabled, required | mue-change | mue-input.js | frm-input-fields |
| <mue-modals> | open, title, width | mue-close, mue-open | mue-modals.js | ui-modals |
| <mue-nav-menu> | label | mue-select, mue-navigate | mue-nav-menu.js | nav-menu |
| <mue-notification> | type, variant, title, message, open | mue-close | mue-notification.js | ui-notifications |
| <mue-pagination> | total, page, window, label | mue-page-change | mue-pagination.js | nav-pagination |
| <mue-popover> | trigger, placement, icon, open | mue-close, mue-open | mue-popover.js | ui-popover |
| <mue-progress> | label, value, min, max, variant, striped, indeterminate | mue-change | mue-progress.js | fb-progress |
| <mue-radio> | label, name, value, checked, disabled | mue-change | mue-radio.js | frm-radio-buttons |
| <mue-radio-group> | label, name, value, disabled | mue-change | mue-radio-group.js | no catalog entry |
| <mue-rating> | value, max, caption, label, interactive, name | mue-change | mue-rating.js | ui-rating |
| <mue-select> | label, name, value, helper, message, state, disabled, required | mue-change | mue-select.js | frm-select-menus |
| <mue-sidebar> | — slot children | mue-navigate | mue-sidebar.js | nav-sidebar |
| <mue-sidebar-heading> | — slot children | mue-navigate | mue-sidebar.js | nav-sidebar |
| <mue-sidebar-link> | — slot children | mue-navigate | mue-sidebar.js | nav-sidebar |
| <mue-skeleton> | variant, width | — | mue-skeleton.js | fb-skeleton |
| <mue-stepper> | current | mue-step | mue-stepper.js | fb-stepper |
| <mue-switch> | label, name, value, checked, disabled | mue-change | mue-switch.js | frm-switches |
| <mue-tab> | — slot children | — | mue-tab.js | no catalog entry |
| <mue-table> | striped, compact | mue-sort | mue-table.js | ui-tables |
| <mue-tabpanel> | — slot children | — | mue-tabpanel.js | no catalog entry |
| <mue-tabs> | selected, label | mue-change | mue-tabs.js | nav-tabs |
| <mue-textarea> | label, name, value, placeholder, helper, message, state, disabled, required, rows | mue-change | mue-textarea.js | no catalog entry |
Attribute and event lists were read out of the 40 modules in frameworks/web-components/components/; the “Module” column names the file, so any row can be checked in seconds. The Catalog entry column comes from frameworks/registry.json, which points each catalog id at one web-components source file: 36 of the 43 tags are named that way, the other 7 (mue-accordion-item, mue-avatar, mue-avatar-group, mue-radio-group, mue-tab, mue-tabpanel, mue-textarea) are registered and shown in the gallery but no catalog entry points at their file. Those links open the vanilla preview page, so its source panel prints HTML rather than a mue-* tag — the gallery above is where the element form is shown.
The 11 events
Every event is a CustomEvent that bubbles from the host, so one listener on a container is enough. Names are prefixed because a bare change would collide with the native event on the <input> inside.
What was run to say any of this
A throwaway project holding nothing but the output of mue add --framework web-components, served over http://127.0.0.1 and driven in headless Chrome. Twelve assertions, twelve passes, re-run the same day this page was written.
| Check | Value the browser reported |
|---|---|
| 3 tags registered | mue-button, mue-input, mue-card all defined by index.js |
| Light DOM | shadowRoot === null on every host |
| Button is a button | BUTTON, type="button", class="btn-primary", background rgb(36, 99, 235), text rgb(255, 255, 255), radius 8px |
| Label is bound | label[for="mue-input-1"] matches input#mue-input-1, type="email" |
| Card uses the sheet | class="card card-elevated", radius 16px, background rgb(255, 255, 255) |
| Token resolves | --color-primary-500: 221 83% 53% on :root, body rgb(248, 250, 252) |
| mue-change fires | typing into the input delivered ["a@b.co"] |
| Click reaches the host | 1 listener call, disabled === false |
| No overflow at 1280px | scroll width 1280 = client width 1280, card 461px |
| No overflow at 390px | scroll width 390 = client width 390, card 358px |
| html.dark reaches them | page rgb(10, 14, 21), button rgb(95, 145, 252), card rgb(35, 42, 52) |
| Console | clean — no errors, no warnings from the modules |
Plus the wider pass that backs the stable label: every preview page built and rendered in Chrome and Edge at 1280px and 390px in light and dark, with axe-core runs over the same pages. See docs/framework-support.md for the full statement.
What this page does not claim
Not published to npm
mue-web-components is the name the folder would ship under; it is not on the registry. Installation is a file copy from a git checkout, described on the CLI page. Publishing is on the roadmap in README.md and has not happened.
No framework interop verified
The React and Vue ports are independent implementations of the same tokens and component classes, written separately. They do not wrap these elements and these elements do not wrap them. No mixed-framework project has been built or tested, so nothing here promises that a React app can mount <mue-card> and keep its props in sync.
Never run on some engines
Chrome and Edge only. Not run: Safari, Firefox, a screen reader, a real touch device, server-side rendering, and a published-package install. Custom elements and form-associated behaviour differ across engines, so a Safari result is genuinely unknown rather than assumed good.
Light DOM is a trade, not a win
Your document stylesheet reaches the elements — that is the benefit. The price is that document-level CSS also reaches into them, and the host rebuilds its children, so a global button { } rule can override btn-primary. The gallery is the reference for which markup each element expects.
The table describes the current commit
Attribute lists were harvested from frameworks/web-components/components/ on 2026-11-01. Run node tools/mue.js usage <id> --framework web-components and the tool prints what the files say today, which is the version you should trust.