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 withaddEventListener.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.
The integration shape
Section titled “The integration shape”Every framework page boils down to the same five steps:
-
Install + import the CSS once at the app entry —
import "@relements/core/index.css";. From here, native elements with.re-*classes anddata-*attributes work with zero JavaScript.<button className="re-button" type="button">Save</button> -
Behaviors — run
enhance*(el)in the framework’s “on mount” primitive against a ref to your subtree, and call the returnedcontroller.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 -
Events — a
re-*CustomEventis a normal bubbling DOM event. Listen with the framework’s event syntax and readevent.detail(e.g.enhanceTabsemitsre-changewith{ tabId, panelId }). -
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.jsonlistselements/*.jsundersideEffectsso thecustomElements.definesurvives tree-shaking), then tell the framework to pass the unknown tag through:Framework Config to allow <re-*>tagsListening to re-changeReact 19 none — renders unknown tags as-is ref+addEventListenerinuseEffectVue isCustomElement: (t) => t.startsWith("re-")@re-change="onChange"Svelte 5 none — unknown tags pass through onre-change={onChange}(case-sensitive)Angular CUSTOM_ELEMENTS_SCHEMAon the component(re-change)="onChange($event)"(cast$event) -
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.
When a behavior restructures the DOM
Section titled “When a behavior restructures the DOM”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.
Pick your framework
Section titled “Pick your framework”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", …).
The example corpus
Section titled “The example corpus”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 itsre-changeevent anddestroy()teardown (the apps mount/unmount the region to prove cleanup); - a
<re-tabs>custom element whosere-changeevent 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.
Related
Section titled “Related”- Behaviors & custom elements — the
full
enhance*contract, the<re-*>elements, and which to reach for. - HTML-first policy — why JS is an optional layer over markup that already works.
- Tabs ·
<re-tabs>— the component used throughout the examples.