minerva-design

Minerva Design: React, Vue, Angular, Web Components and native platform components with shared design tokens and contracts
167 1
Install
npm install minerva-design
Latest Version:1.0.1
Vue:3
License:MIT
Last Updated:Oct 16, 2026
Links: GitHub  ·  npm
Maintainer: wx.f

minerva-design

Platform implementation status

The workspace includes native Vue (minerva-design/vue), Angular (minerva-design/angular) and React Native / Expo (minerva-design/native) renderers. Taro (minerva-design/taro), uni-app (minerva-design/uni) and native WeChat (miniprogram: dist/weapp) also provide native form, navigation, overlay, data-display and theme implementations. The generated platform support matrix records each component’s implementation scope and limitations. Host tests and builds do not imply device certification or npm publication.

The private root is minerva-design-workspace; packages/minerva-design assembles the single published package. packages/core is platform-neutral; browser-specific primitives live in packages/dom. The public /core entry preserves the existing Web API. Sibling md-* directories are linked development worktrees with unfinished changes, not extra packages.

The Minerva design system in one package: accessible React 19 components (SSR / React Server Components ready) and the same component set as framework-agnostic Web Components (Lit) for plain HTML, Vue, Angular, Svelte, Solid or React, with design tokens, theming (light / dark / system, palettes, design presets) and built-in translations (en, zh, ja, fr).

Documentation and live demos: https://fwx5618177.github.io/minerva-design/

Install

pnpm add minerva-design
# or: npm install minerva-design / yarn add minerva-design
  • React DOM entries require React / React DOM 19. The shared package declares optional peers ^18.3.0 || ^19.0.0 because the Taro 4.3 renderer also runs on React 18.3. npm cannot express peers per subpath; that union does not make the React DOM component entry compatible with React 18.
  • Web Components: nothing else to install. Lit is a regular dependency; React-only apps never load it (the web component entries are separate modules).
  • Monaco editor (optional): minerva-design/monaco and minerva-design/web-components/code-editor use the optional peers @monaco-editor/react / monaco-editor.

React

Import the global stylesheet once, in the app entry (src/main.tsx, or the root app/layout.tsx with the Next.js App Router), then the components:

import { Button, ConfigProvider, ToastProvider, toast } from "minerva-design";
import "minerva-design/style.css";

export default function App() {
  return (
    <ConfigProvider>
      <ToastProvider>
        <Button onClick={() => toast.success("Saved")}>Save</Button>
      </ToastProvider>
    </ConfigProvider>
  );
}

Every stylesheet ships inside @layer minerva: your unlayered CSS overrides the library without !important (layered apps: @layer reset, minerva, app;).

Styling hooks (public API): each component renders data-minerva="<component>", data-part="<part>" and state attributes (data-state, data-disabled, data-size, data-variant...), e.g. [data-minerva="button"][data-part="label"]. Repeated items carry their own states on the item element, e.g. [data-minerva="menu"][data-part="item"][data-highlighted], [data-minerva="option"][data-selected] or [data-minerva="data-table"][data-part="header-cell"][data-sort="ascending"]. The hooks of every component are listed on its docs page and in minerva-design/styling-hooks.

Every client module starts with "use client"; minerva-design/utils and minerva-design/theme-utils are server-safe (no "use client", no React) for React Server Components and servers.

Browser iframe adapters can call previewDocument(html) from minerva-design/utils to use the same sanitized, navigation-disabled document as HtmlPreview. Render it in an iframe with sandbox="". Without a DOM, this helper returns an empty document body; generate the sanitized content after hydration rather than sanitizing untrusted HTML on the server with a browser-only sanitizer.

Web Components

// register every element
import "minerva-design/web-components";

// design tokens, once per page (already included in minerva-design/style.css)
import "minerva-design/tokens.css";
<minerva-config theme="system" locale="en">
  <form>
    <label for="email">Email</label>
    <minerva-input
      id="email"
      name="email"
      type="email"
      required
    ></minerva-input>

    <minerva-select name="plan" value="pro" aria-label="Plan">
      <minerva-option value="free">Free</minerva-option>
      <minerva-option value="pro">Pro</minerva-option>
    </minerva-select>

    <minerva-button type="submit">Save</minerva-button>
  </form>
</minerva-config>

<script type="module">
  document
    .querySelector("minerva-select")
    .addEventListener("minerva-change", (event) =>
      console.log(event.detail.value),
    );
</script>

Attributes are kebab-case strings / booleans (full-width, hide-close-button); properties are camelCase and accept any value (arrays, objects and functions are properties only). Setting a property never fires an event; user actions fire minerva-change, minerva-input, minerva-open-change (cancelable: preventDefault() keeps the current state)...

Without a build step

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/minerva-design@0/dist/core/tokens.css"
/>
<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/minerva-design@0/dist/web-components/cdn/minerva.js"
></script>

The CDN bundle is self-contained (every element, Lit and minerva-design/core, minified, production mode).

The code editor runs on your own Monaco engine (never loaded from a CDN) and follows the theme of its scope; until the engine is set (or if it fails) it shows a loading state, then an editable textarea fallback:

import "minerva-design/web-components/code-editor";
import * as monaco from "monaco-editor"; // configure its workers (MonacoEnvironment) in your app

document.querySelector("minerva-code-editor")!.monaco = monaco;

Imperative confirmations: confirm({ title, host: button }) (or const ask = confirmFor(button), then ask({ title })) renders the dialog in the caller's <minerva-config> scope, like the React library's useConfirm(); wrap the app in <minerva-confirm-provider> to queue them per subtree (the React library's ConfirmProvider).

defineElement() is idempotent and does nothing without a customElements registry, so the entries can be imported during server-side rendering.

Typings

Every element is declared in HTMLElementTagNameMap (document.querySelector("minerva-select") is a MinervaSelect). For framework templates:

// React 19 (global.d.ts)
/// <reference types="minerva-design/web-components/react" />
// Svelte 5 (src/app.d.ts)
/// <reference types="minerva-design/web-components/svelte" />
// Solid
/// <reference types="minerva-design/web-components/solid" />

Vue (Volar): add "minerva-design/web-components/vue" to compilerOptions.types in tsconfig.json, and mark the tags as custom elements (isCustomElement: (tag) => tag.startsWith("minerva-")).

VS Code (plain HTML):

{
  "html.customData": [
    "./node_modules/minerva-design/dist/web-components/html-custom-data.json"
  ]
}

Styling

The elements read the design tokens and the same --<component>-* CSS variables as the React components. Their public styling hooks use the shared Minerva vocabulary: the tag, CSS parts and custom states of the host (ElementInternals.states), which add no host attribute:

minerva-button::part(label) {
  letter-spacing: 0.02em;
}
minerva-modal:state(open)::part(content) {
  border: 1px solid var(--border-color);
}
minerva-button:state(size-small):state(variant-ghost) {
  --button-radius: 999px;
}

React's [data-state="open"], [data-disabled] and [data-size="small"] are :state(open), :state(disabled) and :state(size-small) here (Chromium 90-124: :--open).

Items rendered in a shadow root (menu items, options from the options property, table rows and headers, pages, steps, days, toasts, files...) expose their own states as <part>--<state> part names next to the part name (<part>--<key>-<value> for keyed states). ::part() matches elements that carry every listed name, and needs no :state() support:

minerva-menu::part(item item--highlighted) {
  background: var(--primary-color-subtle);
}
minerva-data-table::part(header-cell header-cell--sort-ascending) {
  color: var(--primary-color);
}
minerva-toast-region::part(toast toast--color-success) {
  border-inline-start: 4px solid var(--success-color);
}
/* items that are elements of their own: their custom states */
minerva-option:state(selected)::part(root) {
  font-weight: 600;
}

The hooks of every element are listed on its docs page and in minerva-design/styling-hooks. tokens.css ships inside @layer minerva.

Server rendering and hydration

The modules import in Node without a DOM. Elements in server-rendered markup (for example a React 19 page that renders the tags) upgrade without touching their host attributes: default property values are not reflected (only values you set are), and the implicit ARIA of items (role, aria-selected, aria-checked...) goes through ElementInternals, so frameworks hydrate without attribute mismatches. Properties set before an element is connected (or before its definition loads) are kept. Moving an element in the DOM re-acquires its resources, and an open overlay stays open and working.

Composite items also write host attributes of their own: the roving tabindex of <minerva-tab> / <minerva-radio>, the tabindex of <minerva-tab-panel>, the generated ids and aria-controls / aria-labelledby references between tabs and panels (and option groups and their labels), slot, hidden and data-state / data-selected / data-highlighted / data-disabled. Elements that may be server-rendered markup (upgraded in place, or parsed while the document is loading) defer these writes until hydration has had a chance to run: after the next animation frame, then the next idle period (or task). So you can register the elements before hydrateRoot() and React reports no mismatch. Elements created by script (document.createElement, client rendering) write them right away. Until then, inactive panels hide their content from inside the shadow DOM, and the tabs are not keyboard-focusable yet. The reflected selected (tab) and checked (radio) attributes are written on upgrade; React's hydration ignores those two names. Tab panels using a <template> child stamp it only after that point, so the hydrated light DOM stays unchanged.

Development warnings

Invalid attribute combinations and common mistakes are logged with console.error (the channel and [minerva] <subject>: ... format of the React components of minerva-design: [minerva] <minerva-x>: ...), once per message, when process.env.NODE_ENV !== "production". Bundlers replace that expression, so production builds drop the checks; the CDN bundle is built for production.

Core (advanced)

minerva-design/core is the framework-agnostic, side-effect-free and SSR-safe TypeScript both libraries are built on. It has two layers, shipped once and shared by every entry (an app using React and the web components loads them once):

  • the platform-neutral layer (dist/core, no DOM access, also used by the React Native and mini-program renderers): ids, controllable state, keyboard navigation and typeahead math, theme data and design presets, i18n;
  • the DOM layer (dist/dom, web renderers only; its only runtime dependency is @floating-ui/dom): focus scope, layers, scroll lock, positioning, portal, presence, theme DOM / cookie helpers.
Module Layer API
id neutral createId(prefix?)
controllable neutral createControllableState({ value, defaultValue, onChange })
keyboard neutral getNextIndex(...), createTypeahead()
theme neutral theme types, light / dark / githubDark / themes, palettes, PALETTES, design presets
tokens neutral resolveTokens({ mode, palette, design }), colorMix, generateTokensCss(), token data
i18n neutral messages, SUPPORTED_LANGUAGES, mergeMessages, createTranslator, translate, ...
machines neutral createTabsMachine, createToastQueue, createPaginationMachine, createRatingMachine, ... ({ getState, send, subscribe })
tokens neutral resolveTokens({ mode, palette, design, overrides }), colorMix(a, b, pct), token generators
dom DOM canUseDOM, getActiveElement, contains, getTabbables, getFocusables, focusElement, ...
focus-scope DOM createFocusScope(container, { trapped, loop, autoFocus, restoreFocus })
dismissable-layer DOM createDismissableLayer(element, { onDismiss, branches, disableOutsidePointerEvents, ... })
scroll-lock DOM lockScroll(target?) returns unlock()
hide-others DOM hideOthers(targets, { root, attribute }) returns undo()
roving-focus DOM createRovingFocus(container, options)
portal DOM getPortalContainer(explicit?), createPortalHost({ id, attributes, parent })
positioning DOM computeAnchoredPosition, autoPosition, applyPosition, placement helpers
pointer-grace DOM createPointerGrace(), getGraceArea, isPointInPolygon
presence DOM waitForExitAnimation(el)
theme (DOM) DOM applyThemeStyles, applyDesignAttributes, THEME_INIT_SCRIPT, parseThemeCookies, ...
import {
  autoPosition,
  applyPosition,
  createDismissableLayer,
  createFocusScope,
} from "minerva-design/core";

const trigger = document.querySelector<HTMLElement>("#trigger")!;
const popover = document.querySelector<HTMLElement>("#popover")!;

const stop = autoPosition(
  trigger,
  popover,
  { placement: "bottom-start" },
  (r) => applyPosition(popover, r),
);
const scope = createFocusScope(popover, { trapped: true, loop: true });
scope.activate();
const layer = createDismissableLayer(popover, {
  branches: () => [trigger],
  onDismiss: () => {
    stop();
    scope.deactivate();
    layer.destroy();
    popover.remove();
  },
});

React adapters should use React.useId() rather than createId(), because useId keeps ids stable between the server and client render.

The styling hooks manifest (component → parts → states) and its selector helpers are published as minerva-design/styling-hooks.

Entries

Entry Contents
minerva-design Every React component, hook and theme utility (ESM + CJS, TypeScript types, "use client")
minerva-design/style.css All component styles and the design tokens (import once, in the app entry)
minerva-design/styles/*.css Per-component stylesheets (styles/button.css...; styles/tokens.css for the tokens alone)
minerva-design/tokens.css The design tokens only (e.g. for the web components)
minerva-design/prose.scss The typography mixins of Prose
minerva-design/theme-utils Server-safe theme helpers (THEME_INIT_SCRIPT, THEME_INIT_SCRIPT_HASH, cookie parsing, design presets)
minerva-design/utils Server-safe non-component utilities (cn, themes, palettes, resolveTheme, ...) for Server Components
minerva-design/monaco MonacoCodeEditor (optional peers @monaco-editor/react and monaco-editor)
minerva-design/web-components Registers every custom element; exports the element classes, their types and the authoring utilities (ESM)
minerva-design/web-components/<name> Registers one element (and its parts / dependencies), e.g. /button, /select, /modal, /config
minerva-design/web-components/code-editor Optional: <minerva-code-editor> (Monaco), needs the optional peer monaco-editor; not in the all-in-one / CDN
minerva-design/web-components/cdn Self-contained ES module bundle (dist/web-components/cdn/minerva.js)
minerva-design/web-components/react · /vue · /svelte · /solid Template typings of the custom elements (types only)
minerva-design/custom-elements.json Custom Elements Manifest
minerva-design/html-custom-data.json VS Code HTML custom data
minerva-design/core Advanced: the framework-agnostic primitives (ESM + CJS)
minerva-design/styling-hooks The styling hooks manifest and selector helpers (ESM + CJS)

Links

Browser support

  • React components: React 19 on evergreen browsers (fully supported: Chrome / Edge 120+, Firefox 125+, Safari 17+; down to Chrome 111, Firefox 113, Safari 16.4 with minor styling degradations: color-mix(), :has(), :dir(), container queries). Overlays render in a portal (no Popover API needed).
  • Web Components: ES2022, custom elements v1 and shadow DOM on the same browsers. Feature-detected with fallbacks: the Popover API (overlays fall back to position: fixed), ElementInternals (without it the controls do not take part in forms; element-internals-polyfill works) and constructable stylesheets.
  • Core: ES2020 and DOM APIs only.

The feature matrix and fallbacks are in the repository README.

License

MIT

Related Packages

kanto-ds

Eighty accessible web components for dashboards, admin tools and forms — themeab...

2,229
@ds-mo/ui

CompoMo — composable web components styled with TokoMo design tokens

43,041 1
@abelfuentes/ui-controls

Componentes UI atómicos como Web Components. Sin dependencias; funcionan en Reac...

1,057 0
@parthi033/ui-foundation

A lightweight, framework-agnostic Web Components library built with Stencil

2,287 1

Version History

Version Released Vue License
1.0.1 ^3.5.0 MIT
1.0.0 ^3.5.0 MIT
Pre-releases (1)
Version Released License
0.0.0-stage