# 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. ```ts 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: ```ehtml app/shared/templates/toolbar/index.ehtml import { rovingFocus, type Roving } from "@elements/ui/behaviors"; interface ToolbarState { roving?: Roving; }
ui.roving = rovingFocus(e.target as HTMLElement, { items: "button" })} onremove={() => ui.roving?.release()}>
``` 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](uid) | A page-unique id, `"menu-1"`, for aria wiring. | | `effect(fn)` | [effect](effect) | Runs fn now and whenever a reactive value it read changes; returns stop. | | `untracked(fn)` | [effect](effect) | Calls fn without subscribing to what it reads. | | `watchValue(read, fn)` | [effect](effect) | Runs fn(value, previous) when read() changes, not on the first run. | | `createPresence(read)` | [presence](presence) | A Presence matching open's current value, for a private attribute. | | `presence(p, read, opts)` | [presence](presence) | Keeps the Presence in step with open through enter and exit animations. | | `animationsDone(el)` | [presence](presence) | Resolves when every finite animation on el and its subtree has finished. | | `focusScope(container, opts)` | [focusScope](focus-scope) | Moves focus in, traps it, returns it on release. | | `focusScopeDepth()` | [focusScope](focus-scope) | How many focus scopes are open. | | `rovingFocus(container, opts)` | [rovingFocus](roving-focus) | One Tab stop for a group, arrows between its items. | | `typeahead(timeout)` | [typeahead](typeahead) | A search function: the item whose text starts with what was typed. | | `isTypeaheadKey(e)` | [typeahead](typeahead) | True for one printable character with no modifier. | | `pushLayer(el, opts)` | [pushLayer](dismiss) | A dismiss layer: Escape and outside presses, stacked. | | `layerCount()`, `topLayer()` | [pushLayer](dismiss) | The number of open layers, and the top one. | | `onScrollbar(e)` | [pushLayer](dismiss) | True when a press landed on the pressed element's scrollbar. | | `float(anchor, el, opts)` | [float](floating) | Positions el against anchor and follows until stopped. | | `computeFloat(anchor, el, opts)` | [float](floating) | Finds the position once without moving el. | | `lockScroll(opts)` | [lockScroll](scroll-lock) | Stops the page scrolling; returns unlock. | | `scrollLockCount()` | [lockScroll](scroll-lock) | How many locks are held. | | `hideOthers(keep)` | [hideOthers](hide-others) | aria-hidden on everything outside keep; returns the release. | | `announce(message, politeness, opts)` | [announce](announce) | Speaks a message through a live region. | | `clearAnnouncements(politeness)` | [announce](announce) | Empties one region or both. | | `destroyAnnouncer()` | [announce](announce) | Removes the regions; the next announce creates them again. | | `drag(el, opts)` | [drag](drag) | Pointer and keyboard dragging with threshold, axis and velocity. | | `activeDescendant(focusEl, list, opts)` | [activeDescendant](active-descendant) | A virtual highlight in list while focus stays on focusEl. | | `scrollIntoNearest(item, list)` | [activeDescendant](active-descendant) | Scrolls an item into view inside its list, never the page. | | `gridFocus(grid, opts)` | [gridFocus](grid-focus) | One Tab stop for a grid, arrows by row and column. | | `scrollFocus(root, opts)` | [scrollFocus](scroll-focus) | A scrolling region is a Tab stop while it overflows. | | `tabbables(container)` | [DOM helpers](dom) | Every element Tab can reach, in order. | | `isDisabled(el)`, `isVisible(el)` | [DOM helpers](dom) | Whether an element can be acted on, and whether it renders. | | `hiddenWithRoot(el, root)` | [DOM helpers](dom) | el is inside a root that renders no box: hidden, not gone. | | `focusElement(el, opts)`, `focusFirst(els, opts)` | [DOM helpers](dom) | Focus without scrolling the page. | | `clickPointer(e)` | [DOM helpers](dom) | The pointer behind a click: mouse, touch, pen or none. | | `isInside(target, roots)` | [DOM helpers](dom) | target is inside any of the roots. | | `directionOf(el)`, `textValue(el)` | [DOM helpers](dom) | 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](presence) | `Presence`, `PresenceOptions`, `PresencePhase` | | [focusScope](focus-scope) | `FocusScope`, `FocusScopeOptions`, `FocusManagerOptions`, `FocusTarget` | | [rovingFocus](roving-focus) | `Roving`, `RovingOptions`, `Orientation` | | [typeahead](typeahead) | `Typeahead` | | [pushLayer](dismiss) | `Layer`, `LayerOptions`, `DismissReason` | | [float](floating) | `FloatOptions`, `FloatStop`, `FloatPlacement`, `FloatSize`, `FloatMiddleware`, `MiddlewareState`, `OffsetData`, `OffsetFn`, `Anchor`, `Side`, `LogicalSide`, `Align` | | [lockScroll](scroll-lock) | `ScrollLockOptions` | | [announce](announce) | `Politeness`, `Announcement`, `AnnounceOptions` | | [drag](drag) | `Drag`, `DragOptions`, `DragState`, `DragStep`, `DragKeyboard`, `DragAxis`, `DragSource` | | [activeDescendant](active-descendant) | `ActiveDescendant`, `ActiveDescendantOptions` | | [gridFocus](grid-focus) | `GridFocus`, `GridFocusOptions`, `GridPosition` | | [scrollFocus](scroll-focus) | `ScrollFocusOptions` |