Skip to content

Frameworks

Relements is framework-agnostic with no wrappers. There is no @relements/react or @relements/vue — the published package is just @relements/core, and the same surface works everywhere:

  • .re-* classes on native elements — the zero-JS styling baseline.
  • data-* attributes — declarative hooks that behaviors read.
  • re-* CustomEvents — bubbling DOM events you listen to with addEventListener.
  • enhance*(root) → { destroy() } behaviors — optional JS that wires a subtree and tears itself down.
  • <re-*> light-DOM custom elements — self-registering, self-managing tags.

Because all five are plain web-platform primitives — classes, attributes, DOM events, functions, and custom elements — every framework already speaks them. The only thing that differs per framework is where you call enhance*() on mount and destroy() on unmount, plus a line or two of config so the framework lets <re-*> tags and their events through.

Every framework page boils down to the same five steps:

  1. Install + import the CSS once at the app entry — import "@relements/core/index.css";. From here, native elements with .re-* classes and data-* attributes work with zero JavaScript.

    <button className="re-button" type="button">
    Save
    </button>
  2. Behaviors — run enhance*(el) in the framework’s “on mount” primitive against a ref to your subtree, and call the returned controller.destroy() in “on unmount”. That is the whole lifecycle; behaviors are idempotent and remove every listener they added.

    const controller = enhanceTabs(el); // on mount
    // …
    controller.destroy(); // on unmount
  3. Events — a re-* CustomEvent is a normal bubbling DOM event. Listen with the framework’s event syntax and read event.detail (e.g. enhanceTabs emits re-change with { tabId, panelId }).

  4. Custom elements<re-*> are light-DOM custom elements, so they need no Shadow-DOM workarounds. Register each one with its bare side-effect import (import "@relements/core/elements/re-tabs";package.json lists elements/*.js under sideEffects so the customElements.define survives tree-shaking), then tell the framework to pass the unknown tag through:

    FrameworkConfig to allow <re-*> tagsListening to re-change
    React 19none — renders unknown tags as-isref + addEventListener in useEffect
    VueisCustomElement: (t) => t.startsWith("re-")@re-change="onChange"
    Svelte 5none — unknown tags pass throughonre-change={onChange} (case-sensitive)
    AngularCUSTOM_ELEMENTS_SCHEMA on the component(re-change)="onChange($event)" (cast $event)
  5. Forms / native inputs.re-input, .re-select, .re-textarea, .re-checkbox, … are just classes on native form controls, so they bind with the framework’s ordinary model/state (value/onChange, v-model, bind:value, [(ngModel)]) with no special handling.

That is it. The DOM, class names, --re-* tokens, and event contract are identical across stacks; only the glue in steps 2 and 4 changes.

Almost every behavior only adds nodes — it injects siblings or children around the element you enhanced and never moves your markup, so it’s robust to any re-render or unmount (enhance a stable wrapper and you’re done; that’s how enhanceMultiSelect injects its live region).

One behavior can reparent your markup: applied directly to an <input data-re-tags-input>, enhanceTagsInput moves that input into a chip container it creates — and a vdom framework that owns the input can throw insertBefore … is not a child of this node when it later mutates the input’s slot. Use a framework-safe form, both of which leave the input where you rendered it: the <re-tags-input> custom element (it owns its subtree, so the framework sees an opaque tag), or container mode — put data-re-tags-input on a container that wraps the input, and the behavior injects the chips around it without moving it. The general lesson: a behavior that only adds nodes around your markup is safe; one that moves your markup needs a boundary the framework treats as opaque.

Each page mirrors a runnable example app and walks through the five steps above:

Open any example live, no install — in StackBlitz or CodeSandbox (each framework page has its own one-click buttons).

Not using a framework? The same markup is the baseline everywhere — see plain HTML usage, where <re-*> elements work as-is and you listen with addEventListener("re-change", …).

Every framework page is backed by an app under docs/examples/frameworks/<fw>/ that consumes only the published @relements/core API. All five render the same one flow:

  • a <button class="re-button"> — the CSS class surface;
  • a tabs region enhanced by enhanceTabs() — the behavior surface, with its re-change event and destroy() teardown (the apps mount/unmount the region to prove cleanup);
  • a <re-tabs> custom element whose re-change event updates an <output> — the custom-element + event surface;
  • a multi-select enhanced by enhanceMultiSelect() — the DOM-injecting behavior surface (it appends a live region, survives an in-place re-render, and tears down on unmount);
  • a carousel enhanced by enhanceCarousel(), which appends its controls as trailing children of the host; and
  • a command palette (enhanceDialog() + enhanceCommandPalette()), which applies combobox/listbox ARIA and injects an sr-only status announcer as a sibling — both survive an in-place re-render and tear down on unmount.

The DOM, class names, --re-* tokens, and re-change contract are identical across all five; only the framework glue differs. Browse the source on GitHub.