Behaviors & custom elements
Relements is HTML-first: the foundation is semantic markup plus CSS, and JavaScript is optional. When a component needs interaction beyond what native HTML and CSS provide, you opt into one of two JS layers — an enhance* behavior or a <re-*> custom element. Both progressively enhance markup that already works without them; neither is required for base functionality.
The enhance* pattern
Section titled “The enhance* pattern”Every behavior follows the same contract:
- A single function,
enhance*(root = document), that scansrootfor itsdata-re-*hosts and wires them. - It returns a controller —
{ destroy() }— that removes every listener it added. - It is idempotent: re-running it over an already-enhanced subtree is a no-op, guarded per host (typically a
data-re-*-readymarker). - It is a tree-shakable named ESM export, available both from the root and from a per-behavior subpath (e.g.
@relements/core/behaviors/tabs), so you only ship the behaviors you import. - It is fully typed (each module ships a
.d.ts), androotmay be aDocument,Element, orShadowRoot. - Where it emits events, they are bubbling
CustomEvents prefixedre-*(e.g.re-change,re-select,re-dismiss), so any framework can listen withaddEventListener.
import { enhanceTabs } from "@relements/core/behaviors/tabs";
// Wire every [data-re-tabs] host under document.const controller = enhanceTabs(document);
document.addEventListener("re-change", (e) => { console.log(e.detail); // { tabId, panelId }});
// Later — remove all listeners this call added.controller.destroy();Passing a narrower root scopes the enhancement; this is how framework components enhance just their own subtree on mount and destroy() on unmount:
const controller = enhanceTabs(myComponentRoot);// onUnmount → controller.destroy();Progressive enhancement, not a requirement
Section titled “Progressive enhancement, not a requirement”Behaviors never own the markup — they layer over HTML that already works. The native baseline keeps working with zero JavaScript:
- A
<dialog>opens, closes, and traps focus natively;enhanceDialogonly adds ergonomics like a trigger button and backdrop-click dismissal. - A
<textarea data-autosize>is a normal resizable textarea (and modern browsers grow it in pure CSS viafield-sizing: content);enhanceAutosizeis a no-op fallback for engines without it. <re-popover>/enhancePopoverfeature-detect the Popover API and bail out gracefully where it is absent (see Browser support).- A combobox stays a native
<input list>+<datalist>; the password toggle button is authoredhiddenand only un-hidden by JS, so no dead control is ever shown.
Declarative data-re-* hooks
Section titled “Declarative data-re-* hooks”Because behaviors find their hosts by data-re-* attributes rather than by imperative wiring, a single global init can enhance a whole page with no per-page JavaScript — you just author the right attributes. The documentation site does exactly this: its client init (src/client/enhance.ts) imports the behaviors once and runs each over document, so any demo that is written declaratively becomes interactive on the site without an inline script.
<!-- No page script needed — a global enhanceDialog wires this. --><button type="button" data-re-dialog-trigger data-re-dialog-target="confirm">Open</button><dialog id="confirm" data-re-dialog-close-on-backdrop> … <button data-re-dialog-close value="cancel">Cancel</button></dialog>Genuinely imperative APIs have no declarative form. showToast is a function you call, not markup you author, so the docs site wires demo buttons with a docs-only hook and shows the real showToast(...) call alongside.
The behaviors
Section titled “The behaviors”All 17 behaviors are exported by name from @relements/core and from @relements/core/behaviors/<name>.
Overlays & menus
Section titled “Overlays & menus”enhanceDialog— ergonomics for native<dialog>:data-re-dialog-trigger/-targetto open,data-re-dialog-closebuttons, optional backdrop-click anddata-re-dialog-no-dismiss. NativeshowModal/Escape/focus stay native. See Dialog, Alert dialog, Drawer.enhanceMenuButton— the ARIA menu-button pattern (toggle, roving focus, typeahead, outside-click close); emitsre-select({ item, value }). See Menu button.enhancePopover— anchored positioning for native[popover]and are-toggleevent mirroring the nativetoggle. No-ops without the Popover API. See Popover.enhanceContextMenu— opens a styledrole="menu"at the pointer on right-click (and ContextMenu key / Shift+F10); emitsre-select. Falls back to the native browser menu with no JS. See Context menu.enhanceCommandPalette— turns a<dialog>into a filterable command launcher (combobox/listbox ARIA, type-to-filter, optional hotkey); emitsre-command. ReusesenhanceDialogfor the modal lifecycle. See Command palette.
Navigation & roving focus
Section titled “Navigation & roving focus”enhanceTabs— the ARIA tabs pattern with automatic activation and Arrow/Home/End keys; emitsre-change({ tabId, panelId }). See Tabs.enhanceToolbar— collapses arole="toolbar"to one Tab stop with Arrow-key roving; composes with a hosted menu. See Toolbar.
Form inputs
Section titled “Form inputs”enhanceCombobox— a styledrole="listbox"over a native<input list>+<datalist>; case-insensitive filtering and the ARIA editable-combobox pattern. Commits dispatch nativeinput/change. See Combobox.enhanceNumberStepper— wires large +/− buttons to a native number input (nativestepUp/stepDown, theninput/change). See Number stepper.enhancePasswordToggle— a show/hide button that flips a password field’stypeand reflectsaria-pressed; preserves caret. See Password toggle.enhanceAutosize— grows a.re-textarea[data-autosize]with its content; a no-op where CSSfield-sizing: contentis supported. See Autosize textarea.enhanceOtp— autofill-safe polish for a one-time-code input (active-cell hook, optional digit-strip); never splits the field. See OTP.enhanceTagsInput— turns a text input into a token/chip editor that submits an array; emitsre-tags-change({ values }) +change. See Tags input.enhanceRating— normalizes arrow-key direction in a star-rating radio group across browsers. See Rating.enhanceRange— turns two overlaid range inputs into a two-thumb min–max slider; no custom events (nativeinput/changebubble). See Range.
Dismissal & notifications
Section titled “Dismissal & notifications”enhanceDismissible—[data-re-dismiss]buttons hide their[data-re-dismissible]ancestor; emits cancelablere-dismiss. See Banner, Alert.showToast(message, options)— imperative; appends a toast to a[data-re-toast-region](creating one ondocument.bodyif absent) and returns{ dismiss, element }. Not anenhance*function — there is no markup to wire. See Toast.
Custom elements
Section titled “Custom elements”Four <re-*> custom elements wrap the behaviors above for consumers who prefer a tag over an imperative call. They are:
- Light-DOM only — no Shadow DOM. Your markup stays in the light tree, so component CSS, page styles, forms, and
querySelectorall work normally. - Self-registering on import — importing the module calls
customElements.define()as a side effect. Import each element you use;package.jsonlistselements/*.jsundersideEffectsso bare imports survive tree-shaking (see HTML-first policy). - The same class/attribute/event contract — on connect, each host applies its own
.re-*class anddata-re-*marker, then runs its behavior over itself and tears down on disconnect. The element re-dispatches the behavior’sre-*events, so the HTML and the custom-element APIs converge.
import "@relements/core/elements/re-tabs";| Element | Wraps | Exposes |
|---|---|---|
<re-tabs> | enhanceTabs | value property (selected tab id; set to switch); re-dispatches re-change. Observes children so frameworks that project late still enhance. |
<re-menu> | enhanceMenuButton | open boolean property; re-dispatches re-select. |
<re-popover> | enhancePopover | show() / hide() / toggle() methods; open property (reflects :popover-open). Adds the native popover attribute on connect. |
<re-toast> | showToast | .show(message, options) method scoped to its own region; materializes a .re-toast-region on connect. |
Behavior vs custom element — which to reach for
Section titled “Behavior vs custom element — which to reach for”Both wire the same logic; pick by integration style.
Reach for a behavior when you want to enhance existing markup imperatively and 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 the form inputs and dismissal helpers, which have no custom-element wrapper.
Reach for a custom element when you want a declarative, self-managing tag — drop <re-tabs> into any template and it enhances and cleans itself up via connectedCallback/disconnectedCallback, no init call to remember. It is the natural fit for plain HTML pages and for frameworks that render custom elements directly. The four elements wrap tabs, menu, popover, and toast; everything else is behavior-only.
Either way the underlying markup is the same semantic HTML, so you can start with one and switch later without changing the document’s structure.
Related
Section titled “Related”- HTML-first policy — why JS is an optional layer.
- Browser support — what degrades gracefully where a platform feature is missing.
- Versioning —
re-*events,data-re-*hooks, and<re-*>tags are public API.