Manual UI Behaviors

Behaviors

elements man ui/behaviors

Behaviors are reusable user interface functions that handle focus, the keyboard, positioning and dismissal for you. Trap focus inside a dialog (focusScope). Move focus through a toolbar or a list with the arrow keys (rovingFocus). Jump to an item by typing its first letters (typeahead). Close a popup on Escape or a click outside it (pushLayer). Position a popup beside its trigger and keep it on screen (float). Animate an element in and out (presence). Stop the page scrolling behind a modal (lockScroll). Hide the rest of the page from screen readers (hideOthers). Announce a change to a screen reader (announce). Drag items with the pointer or the keyboard (drag). Track the active option in a list (activeDescendant). Each is a function you call on a DOM element from a template of your own. The shipped templates call the same functions, so a popup of yours closes and stacks the way a menu's does, and a scroll lock yours takes nests with a dialog's.

Import
import { focusScope, pushLayer, lockScroll } from "@elements/ui/behaviors";

Each behavior is a function that takes DOM elements and returns a release function, or an object with release(). Start it in oninsert and release it in onremove. A controller object held in a private attribute carries the handle between the two:

app/shared/templates/toolbar/index.ehtml
import { rovingFocus, type Roving } from "@elements/ui/behaviors"; interface ToolbarState { roving?: Roving; } <Toolbar (label: string, private ui: ToolbarState = {})> <div role="toolbar" aria-label={label} oninsert={(e) => ui.roving = rovingFocus(e.target as HTMLElement, { items: "button" })} onremove={() => ui.roving?.release()}> <slot/> </div> </Toolbar>

A behavior writes to the DOM (a tabindex, a data attribute, a CSS variable) and puts back what it changed on release. Each page lists what its behavior writes, so a template never binds the same attribute itself. Behaviors that stack (dismiss layers, focus scopes, scroll locks, hidden siblings) count their nesting, so a menu inside a dialog closes on Escape before the dialog does, and the page unlocks when the last lock goes.

Exports

Export Page What it does
uid(prefix) uid (opens in a new tab) A page-unique id, "menu-1", for aria wiring.
effect(fn) effect (opens in a new tab) Runs fn now and whenever a reactive value it read changes; returns stop.
untracked(fn) effect (opens in a new tab) Calls fn without subscribing to what it reads.
watchValue(read, fn) effect (opens in a new tab) Runs fn(value, previous) when read() changes, not on the first run.
createPresence(read) presence (opens in a new tab) A Presence matching open's current value, for a private attribute.
presence(p, read, opts) presence (opens in a new tab) Keeps the Presence in step with open through enter and exit animations.
animationsDone(el) presence (opens in a new tab) Resolves when every finite animation on el and its subtree has finished.
focusScope(container, opts) focusScope (opens in a new tab) Moves focus in, traps it, returns it on release.
focusScopeDepth() focusScope (opens in a new tab) How many focus scopes are open.
rovingFocus(container, opts) rovingFocus (opens in a new tab) One Tab stop for a group, arrows between its items.
typeahead(timeout) typeahead (opens in a new tab) A search function: the item whose text starts with what was typed.
isTypeaheadKey(e) typeahead (opens in a new tab) True for one printable character with no modifier.
pushLayer(el, opts) pushLayer (opens in a new tab) A dismiss layer: Escape and outside presses, stacked.
layerCount(), topLayer() pushLayer (opens in a new tab) The number of open layers, and the top one.
onScrollbar(e) pushLayer (opens in a new tab) True when a press landed on the pressed element's scrollbar.
float(anchor, el, opts) float (opens in a new tab) Positions el against anchor and follows until stopped.
computeFloat(anchor, el, opts) float (opens in a new tab) Finds the position once without moving el.
lockScroll(opts) lockScroll (opens in a new tab) Stops the page scrolling; returns unlock.
scrollLockCount() lockScroll (opens in a new tab) How many locks are held.
hideOthers(keep) hideOthers (opens in a new tab) aria-hidden on everything outside keep; returns the release.
announce(message, politeness, opts) announce (opens in a new tab) Speaks a message through a live region.
clearAnnouncements(politeness) announce (opens in a new tab) Empties one region or both.
destroyAnnouncer() announce (opens in a new tab) Removes the regions; the next announce creates them again.
drag(el, opts) drag (opens in a new tab) Pointer and keyboard dragging with threshold, axis and velocity.
activeDescendant(focusEl, list, opts) activeDescendant (opens in a new tab) A virtual highlight in list while focus stays on focusEl.
scrollIntoNearest(item, list) activeDescendant (opens in a new tab) Scrolls an item into view inside its list, never the page.
gridFocus(grid, opts) gridFocus (opens in a new tab) One Tab stop for a grid, arrows by row and column.
scrollFocus(root, opts) scrollFocus (opens in a new tab) A scrolling region is a Tab stop while it overflows.
tabbables(container) DOM helpers (opens in a new tab) Every element Tab can reach, in order.
isDisabled(el), isVisible(el) DOM helpers (opens in a new tab) Whether an element can be acted on, and whether it renders.
hiddenWithRoot(el, root) DOM helpers (opens in a new tab) el is inside a root that renders no box: hidden, not gone.
focusElement(el, opts), focusFirst(els, opts) DOM helpers (opens in a new tab) Focus without scrolling the page.
clickPointer(e) DOM helpers (opens in a new tab) The pointer behind a click: mouse, touch, pen or none.
isInside(target, roots) DOM helpers (opens in a new tab) target is inside any of the roots.
directionOf(el), textValue(el) DOM helpers (opens in a new tab) The writing direction in effect, and the text a typeahead matches.

Types

Each page lists the types its behavior exports. The names, for an import:

Page Types
presence (opens in a new tab) Presence, PresenceOptions, PresencePhase
focusScope (opens in a new tab) FocusScope, FocusScopeOptions, FocusManagerOptions, FocusTarget
rovingFocus (opens in a new tab) Roving, RovingOptions, Orientation
typeahead (opens in a new tab) Typeahead
pushLayer (opens in a new tab) Layer, LayerOptions, DismissReason
float (opens in a new tab) FloatOptions, FloatStop, FloatPlacement, FloatSize, FloatMiddleware, MiddlewareState, OffsetData, OffsetFn, Anchor, Side, LogicalSide, Align
lockScroll (opens in a new tab) ScrollLockOptions
announce (opens in a new tab) Politeness, Announcement, AnnounceOptions
drag (opens in a new tab) Drag, DragOptions, DragState, DragStep, DragKeyboard, DragAxis, DragSource
activeDescendant (opens in a new tab) ActiveDescendant, ActiveDescendantOptions
gridFocus (opens in a new tab) GridFocus, GridFocusOptions, GridPosition
scrollFocus (opens in a new tab) ScrollFocusOptions