Skip to content

Overview

A handful of patterns ship as <re-*> custom elements — light-DOM web components that wrap an enhance* behavior in a self-managing tag. Drop the tag into any template and it enhances its own markup on connect and cleans up on disconnect, with no init call to remember.

They are a convenience layer, not a new contract: the inner markup is the same semantic HTML with the same .re-* classes, so an element and its behavior are interchangeable. There are four of them — everything else is behavior-only.

Each element is a side-effect import that registers the tag with the browser. Import it once, anywhere in your bundle — the registration is global and guarded, so importing twice is a no-op:

// Registers <re-tabs> for the whole page.
import "@relements/core/elements/re-tabs";

After that the tag works declaratively in plain HTML or any framework template — no per-instance wiring:

<re-tabs aria-label="Account">
<div role="tablist">
<button role="tab" id="t-1" aria-controls="p-1" aria-selected="true">Profile</button>
<button role="tab" id="t-2" aria-controls="p-2" aria-selected="false" tabindex="-1">
Security
</button>
</div>
<section role="tabpanel" id="p-1" aria-labelledby="t-1"></section>
<section role="tabpanel" id="p-2" aria-labelledby="t-2" hidden></section>
</re-tabs>

Each element exposes a small imperative API (a property or a method) and emits the same bubbling re-* CustomEvents as its behavior, so any framework can read state and listen with plain addEventListener:

const tabs = document.querySelector("re-tabs");
tabs.value = "t-2"; // select the Security tab
tabs.addEventListener("re-change", (e) => console.log(e.detail)); // { tabId, panelId }

Registration is not part of the root @relements/core import — pull in each element’s subpath so you only ship the tags you use.

ElementWrapsJS APIEmits
<re-tabs>enhanceTabselement.value — read/write the selected tab idre-change ({ tabId, panelId })
<re-menu>enhanceMenuButtonelement.open — reflect/control the open statere-select ({ item, value })
<re-popover>enhancePopover.show() / .hide() / .toggle() — native popover APIre-toggle
<re-toast>showToast.show(message, options) — returns the toast controller

Both wire the same logic — pick by integration style:

  • Reach for a custom element when you want a declarative, self-managing tag. It enhances and cleans itself up via connectedCallback / disconnectedCallback, so there is no lifecycle to manage. This is the natural fit for plain HTML pages and for frameworks that render custom elements directly.
  • Reach for the enhance* behavior when you want to control the lifecycle yourself — a global page init, a framework component enhancing its own subtree on mount and calling destroy() on unmount, or scoping enhancement to a ShadowRoot. Behaviors are also the only option for patterns with no element wrapper.

Because the underlying markup is identical, you can start with one and switch later without touching the document’s structure. The behaviors & custom elements guide covers the full trade-off and the enhance* contract in depth.