Skip to content

mak UI web components

@acme/elements is mak UI as framework-agnostic custom elements (built with Lit), for Vue, Svelte, Angular and plain HTML. They read the same token CSS variables as the React components, so every theme mode (light, dark, high contrast) works with no extra setup.

Install

Terminal
pnpm add @acme/elements @acme/tokens

Load the tokens once, then import the package to register every <acme-*> element:

TypeScript
import '@acme/tokens/variables.css'
import '@acme/elements'

Without a bundler, use the single-file build (@acme/elements/bundle, 37 KB minified):

HTML
<link rel="stylesheet" href="/tokens/variables.css" />
<script type="module" src="/makenzie-ui.js"></script>

Vue

Tell the Vue compiler that acme- tags are custom elements, and import @acme/elements before createApp(...).mount() so Vue sets properties rather than attributes.

TypeScript
// vite.config.ts
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('acme-') } } })
HTML
<acme-switch :checked="digest" @change="digest = $event.target.checked">Weekly digest</acme-switch>

Svelte

No configuration needed. Props and onchange / onclick handlers work as on native elements. Svelte's a11y lint can't see the native button inside <acme-button>, so a click handler on it needs <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->.

Svelte
<acme-switch checked={digest} onchange={(e) => (digest = e.currentTarget.checked)}>Weekly digest</acme-switch>

Elements

ElementAttributesNotes
acme-buttonvariant (primary, secondary, outline, ghost, destructive), size (sm, md, lg), type, disabled, loading, labeltype="submit" submits its form, including through the form attribute. label names an icon-only button.
acme-inputlabel, description, error, type, name, value, placeholder, autocomplete, required, disabledLabel, description and error share a shadow root with the input, so they are associated.
acme-checkboxname, value, checked, indeterminate, disabledText content is the label. Fires change.
acme-switchname, value, checked, disabledrole="switch". Applies immediately; use a checkbox inside forms.
acme-dialogheading, openNative modal <dialog>: focus trap, Esc, focus return. show() / close(). Buttons in the footer slot with data-close close it. Fires close.
acme-alertvariant (info, success, warning, destructive)title slot. Warnings and errors are role="alert"; info and success are role="status".
acme-cardnonetitle, description and footer slots. Put a real heading in title.
acme-badgevariant (default, secondary, outline, destructive, success, warning, info)The text must carry the meaning on its own.

Forms

Inputs, checkboxes, switches and buttons are form-associated. They submit with FormData, take part in native validation (an invalid field blocks submit and shows its message), and reset with the form. No framework bindings are needed:

TypeScript
form.addEventListener('submit', (e) => {
  e.preventDefault()
  const data = new FormData(form) // includes every <acme-*> field with a name
})

Styling

Elements are styled only with token variables, so theme with the tokens, not by overriding element CSS. Box styles live inside the shadow root, so page resets such as Tailwind's preflight don't strip them. For a one-off adjustment, each element exposes a part: ::part(button), ::part(input), ::part(label) (checkbox, switch), ::part(dialog), ::part(card), ::part(badge), ::part(alert).

Limits

Reference apps

apps/ref-vue (Vue 3) and apps/ref-svelte (Svelte 5) build the same account-settings page from these elements. Both run one shared Playwright suite (tests/reference-apps): axe in all four theme modes, 320px reflow, form validation and submit, and the modal dialog.

Terminal
pnpm --filter ref-vue build && pnpm --filter ref-vue test:a11y

Tests

pnpm --filter @acme/elements test:a11y runs the elements in Chromium in all four theme modes: axe (WCAG 2.2 AA), keyboard, form submission and validation, the dialog, and styling under a page CSS reset.