DOM and accessibility
Own DOM handles safely with refs, directives, focus utilities, transitions, observers, and explicit cleanup.
Use refs for direct DOM ownership
createRef returns a mutable object whose current value follows one intrinsic element. Tavo.js assigns it on mount and hydration, moves it when the backing node changes, and clears it on replacement or unmount.
Object refs are convenient controller fields. Callback refs receive the node and later receive null during cleanup.
mergeRefscombines multiple ref owners into one callback ref.createListRefscreates stable object refs by a string or numeric key; delete removed keys and clear the collection when its owner is disposed.setRefis useful when composing a higher-level component or adapter that forwards a DOM node.Refs are never serialized into server HTML and remain null during server rendering.
tsximport {
createRef,
createTavo,
TavoController,
} from "@tavojs/core";
class SearchFieldController extends TavoController {
input = createRef<HTMLInputElement>();
onMount() {
this.input.current?.select();
}
}
export const SearchField = createTavo({
controller: SearchFieldController,
view({ controller }) {
return (
<label>
Search
<input ref={controller?.input} type="search" />
</label>
);
},
});tsximport {
createListRefs,
createRef,
createTavo,
mergeRefs,
TavoController,
} from "@tavojs/core";
type Result = { id: string; label: string };
class ResultListController extends TavoController {
items = createListRefs<string, HTMLLIElement>();
measuredItem = createRef<HTMLLIElement>();
featuredItem = mergeRefs(
this.items.get("featured"),
this.measuredItem,
);
onUnmount() {
this.items.clear();
}
}
export const ResultList = createTavo<
{ results: Result[] },
Record<string, never>,
ResultListController
>({
controller: ResultListController,
view({ props, controller }) {
return (
<ul>
{props.results.map((result) => (
<li
key={result.id}
ref={
result.id === "featured"
? controller?.featuredItem
: controller?.items.get(result.id)
}
>
{result.label}
</li>
))}
</ul>
);
},
});Attach reusable behavior with directives
An ElementDirective receives an HTMLElement after it mounts and may return cleanup. Pass one directive or an array through the intrinsic use prop. When the directive value changes, Tavo.js cleans up the old value before applying the new one; unmount also runs cleanup.
autoFocusqueues focus after mount and accepts normal FocusOptions.transitionapplies enter immediately, enterActive in a microtask, and leave classes/callbacks during cleanup.transition does not wait for a CSS duration before removing a node. Use it for state classes and callbacks, not as an exit-animation coordinator.
Create reusable behavior with
createDirectiveand attach it through the intrinsic use prop. UsesetRefwhen a component or adapter must forward a DOM node to another ref owner.
tsximport {
autoFocus,
createDirective,
transition,
} from "@tavojs/core";
const announce = createDirective<HTMLElement>((element) => {
element.setAttribute("aria-live", "polite");
return () => {
element.removeAttribute("aria-live");
};
});
const focusNotice = autoFocus();
const revealNotice = transition({
classes: {
enter: "notice--enter",
enterActive: "notice--visible",
leave: "notice--leave",
},
});
export function LiveNotice({ message }: { message: string }) {
return (
<div
tabIndex={-1}
use={[announce, focusNotice, revealNotice]}
>
{message}
</div>
);
}Give dialogs explicit focus ownership
Accessible overlays need an initial focus target, contained Tab navigation, and restoration when the overlay closes. Keep every listener and restoration function under the same component owner.
getFocusableElementsreturns visible links, buttons, enabled form controls, and eligible tabindex elements in DOM order.focusFirstreturns the focused element or null.focusFirstInvalidtargets the first control matching :invalid.trapFocusreturns the keydown-listener cleanup. If no child is focusable, the container itself must be focusable.Focus trapping alone does not provide a complete modal: also label the dialog, prevent background interaction, support Escape where appropriate, and restore focus.
tsximport {
captureFocusRestore,
createRef,
createTavo,
focusFirst,
TavoController,
trapFocus,
} from "@tavojs/core";
import type { PropsWithChildren } from "@tavojs/core";
class DialogController extends TavoController {
dialog = createRef<HTMLDivElement>();
onMount() {
const restoreFocus = captureFocusRestore();
const dialog = this.dialog.current;
if (!dialog) {
return restoreFocus;
}
focusFirst(dialog);
const stopTrap = trapFocus(dialog);
return () => {
stopTrap();
restoreFocus();
};
}
}
export const Dialog = createTavo<
PropsWithChildren<{ label: string }>,
Record<string, never>,
DialogController
>({
controller: DialogController,
view({ props, controller }) {
return (
<div
ref={controller?.dialog}
role="dialog"
aria-modal="true"
aria-label={props.label}
tabIndex={-1}
>
{props.children}
</div>
);
},
});Observe elements with a managed owner
The standalone observer helpers observeResize, observeIntersection, and observeMutation accept an element or ref and return a disconnect function. Inside a TavoController, the matching methods automatically register that disconnect function for component teardown.
observeResizeandobserveIntersectionaccept an Element orDomRefObject.observeMutationaccepts a Node or a ref object.Browser support failures surface from the platform constructors; add a feature check or polyfill when supporting older environments.
Do not start observers during model creation, controller construction, or SSR.
tsxclass ChartController extends TavoController {
chart = createRef<HTMLDivElement>();
onMount() {
this.observeResize(this.chart, () => {
this.measureChart();
});
this.observeIntersection(this.chart, (entries) => {
this.model.patch({ visible: entries[0]?.isIntersecting ?? false });
});
}
measureChart() {
// Read the committed chart dimensions.
}
}Verify cleanup and keyboard behavior
Mount, replace, and unmount the element; assert object refs and callback refs are cleared.
Change a use prop and confirm the previous directive cleanup runs before the new directive.
Tab forward and backward through a focus trap, test an empty trap, and confirm focus restoration.
Unmount an observed component and assert the observer disconnects.
Run the same component through SSR and confirm ref, use, transition, and event instructions do not become HTML attributes.
Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.