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 20 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.enhanceCarousel— back-fills prev/next + a dot strip over a native CSS scroll-snap track (.re-carousel), tracks the active slide by box geometry (RTL-safe), inerts off-screen slides, and announces the settled slide. Optionaldata-re-carousel-autoplayadds an auto-advance timer with a Pause/Play button (WCAG 2.2.2), pausing on hover/focus/hidden-tab and starting paused under reduced motion. No custom event — derive the index from nativescroll. See Carousel.
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.enhanceFilePicker— echoes the filenames, wires drag-and-drop, clear, andaccept/size/count validation over a.re-file-picker; the native input stays the form value. Emitsre-erroron a rejected drop (value changes use nativechange). See File picker.enhanceMultiSelect— over a.re-multiselect(<details>of checkboxes): writes the live “N selected” summary, closes on Escape / outside-click, and enforcesdata-re-multiselect-required(blocks submit,aria-invalidon the<fieldset>, reveals the message). The checkboxes stay the form value; value changes use nativechange. See Multi-select.
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”Six <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. |
<re-file-picker> | enhanceFilePicker | files property (read/write the FileList) and clear() method; re-dispatches re-error when files are rejected. |
<re-tags-input> | enhanceTagsInput | values property (read-only string[]) and clear() method; the element IS the group (container mode), so re-tags-change fires on it. |
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 most form inputs and the 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 six elements wrap tabs, menu, popover, toast, the file picker, and the tags input; 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.