Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Get started

Eunomia is one library of ready-made pieces, buttons, inputs, tabs, and more, built as standard web components. There's no separate React version or Vue version to choose between: the same package works the same way everywhere, in React, Vue, Angular, Next.js, or a plain HTML page, because a web component is understood by the browser itself rather than by one particular framework. Getting a component running takes four steps: install the package, import the one component you need, drop its tag into your markup, and add a theme so it looks right.

1. Install

npm install @eunomia/elements

That's the only thing to install. Lit, the small library each component is built with, is already bundled inside every component's file, so there's nothing extra to add or keep in sync.

2. Import a component

Every component is a named export of the package, and importing one is what makes its tag usable: it registers the element with the browser as a side effect of the import. A bundler only bundles the components you actually import, so your app never ships code for ones it doesn't render.

import {
  EunomiaButton,
  EunomiaTabGroup,
  EunomiaTab,
  EunomiaTabPanel,
} from "@eunomia/elements";

Importing EunomiaButton makes <eun-button> available. Tabs need three tags working together as one system, <eun-tab-group>, <eun-tab>, and <eun-tab-panel>, so import all three (see Tabs).

This import registers the element with the browser as a side effect, which means it needs a real browser window to exist. In a framework that renders on the server (Next.js, Nuxt, SvelteKit, Angular Universal, or any other SSR setup), a plain top-level import evaluated during the server render throws ReferenceError: customElements is not defined, even for a Server Component that only imports a utility like initializeTheme from the same package. Keep every import behind your framework's client-only boundary (Next.js' "use client", Nuxt's <ClientOnly>/.client.ts, or the equivalent elsewhere) or inside a browser-only lifecycle hook (useEffect, onMounted, ...). The Next.js tab below shows the exact pattern; it carries over directly to any other SSR framework.

3. Use the tag

Pick a framework in the toolbar above and every snippet on this page adapts.

Confirm
<eun-button variant="primary">Confirm</eun-button>

<eun-tab-group>
  <eun-tab slot="navigation" panel="a">First</eun-tab>
  <eun-tab slot="navigation" panel="b">Second</eun-tab>
  <eun-tab-panel name="a">First panel.</eun-tab-panel>
  <eun-tab-panel name="b">Second panel.</eun-tab-panel>
</eun-tab-group>

This is plain HTML, so it works with any framework, or with none at all.

4. Theme it

Every color, corner rounding, spacing, and shadow a component uses comes from a shared set of design values behind the scenes, never a color or size baked directly into the component. Skip this step entirely and components still render just fine, using sensible built-in defaults, the same ones shown throughout this documentation site.

To ship a real theme instead, include one of the 7 ready-made palettes:

<link
  rel="stylesheet"
  href="node_modules/@eunomia/elements/styles/themes/prebuilt-themes/violet/eunomia-violet.theme.scss"
/>
<body class="eunomia-theme eunomia-violet-theme">
  <app-root></app-root>
</body>

The full picture, dark mode, all 7 palettes, and generating a theme from your own two brand colors instead, is in Theming and Prebuilt themes.

TypeScript

@eunomia/elements ships its own type declarations, so a class like EunomiaButton and its properties are already typed the moment you import it. Each component's module also augments the global HTMLElementTagNameMap with its own tag as soon as it's imported, so document.createElement("eun-button") is already typed correctly too, the moment anything from the package has been imported anywhere in your program: there's nothing to write for that yourself.

JSX and TSX

Writing <eun-button variant="primary"> directly in a .tsx file needs one more step, since JSX reads its own separate JSX.IntrinsicElements type rather than HTMLElementTagNameMap. Import @eunomia/elements/jsx once, anywhere in your program:

import type {} from "@eunomia/elements/jsx";

It's a type-only import, so it carries no runtime cost and never registers a custom element itself: a good place for it is right next to your app's other global type declarations. Every eun-* tag then becomes a typed JSX element, with the same property autocomplete and invalid-value rejection as the component's own class.

Typed custom events

An event like eunnavigate stays a plain CustomEvent, since it's built for addEventListener rather than a JSX onXxx prop. In React, read it off a ref instead:

import { useEffect, useRef } from "react";
import { EunomiaButton } from "@eunomia/elements";

function ConfirmButton() {
  const ref = useRef(null);

  useEffect(() => {
    const button = ref.current;
    const handleNavigate = (event) => {
      console.log(event.detail);
    };
    button?.addEventListener("eunnavigate", handleNavigate);
    return () => button?.removeEventListener("eunnavigate", handleNavigate);
  }, []);

  return (
    <eun-button ref={ref} variant="primary">
      Confirm
    </eun-button>
  );
}

Each component's own page lists its full set of events (for example Button: API), with the shape of each one's detail.

Accessibility

Keyboard focus, screen reader announcements, and the right ARIA attributes are already built into every component: nothing to add yourself. Each component's own page has an Accessibility tab (for example Button: Accessibility) spelling out exactly which keyboard shortcuts and ARIA behavior it implements.

Troubleshooting

Its file hasn't been imported yet, so the browser doesn't recognize the tag at all. Double-check that the import for that component actually ran: no typo in the path, not removed by tree-shaking, and not sitting behind a condition that never executes.

See the Angular tab above: add CUSTOM_ELEMENTS_SCHEMA. Other strict template compilers usually offer their own equivalent escape hatch for allowing custom elements through.

A component's file is being imported during a server render: a Next.js Server Component, a Nuxt page rendered without <ClientOnly>, a SvelteKit load function, an Angular Universal render, or any other SSR/static-generation pass. Move the import behind that framework's client-only boundary instead (see the SSR warning and the Next.js/Vue/Angular tabs above).

Confirm that data-brand and data-theme are set on <html> (this site's own convention, see Theming), or that the eunomia-theme/eunomia-<name>-theme classes are present on <body> if you're loading the library's stylesheets directly instead. A component with no theme applied at all still renders fine, just with its built-in default colors, which can look like theming "isn't working" if you were expecting a different brand.

Check that you're actually setting data-theme="dark", rather than toggling a class of your own; see Theming: How switching works.

Next steps

Components

Browse everything available today

Theming

7 prebuilt themes, light and dark mode, or your own brand colors

Foundation

Spacing, radius, shadows, typography, and color tokens

Web components

Why Lit, and how framework-agnostic components work