Navigated to /docs/core/dom-and-accessibility

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.

  • mergeRefs combines multiple ref owners into one callback ref.

  • createListRefs creates stable object refs by a string or numeric key; delete removed keys and clear the collection when its owner is disposed.

  • setRef is 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.

TSX
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>
    );
  },
});
TSX
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.

  • autoFocus queues focus after mount and accepts normal FocusOptions.

  • transition applies 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 createDirective and attach it through the intrinsic use prop. Use setRef when a component or adapter must forward a DOM node to another ref owner.

TSX
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.

  • getFocusableElements returns visible links, buttons, enabled form controls, and eligible tabindex elements in DOM order.

  • focusFirst returns the focused element or null. focusFirstInvalid targets the first control matching :invalid.

  • trapFocus returns 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.

TSX
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.

  • observeResize and observeIntersection accept an Element or DomRefObject.

  • observeMutation accepts 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.

TSX
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.