Vize

focus

Track and control focus of a single element.

Package @vizejs/composable/focus
Own the source vize lib pull composable:focus
Runtime exports useFocus, useFocusWithin
Gzip budget 2560 B

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/composable

Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.

import { useFocus, useFocusWithin } from "@vizejs/composable/focus";

Try it

Track and control workshop profile focus

Distinguish one field's focus from focus anywhere in its group, and control that field through a writable focus ref.

Open the interactive example

What to observe: Focus badge name moves actual browser focus into the name field. Escape in that field assigns false to blur it. Tab to Workshop contact: name focus becomes no while profile focus stays yes. Tab outside the group clears both flags. Stop focus tracking freezes the last observed flags and disables the focus controls; the fields remain editable.

useFocus returns a writable computed ref: assigning true calls the target element's focus(), and false calls blur(). useFocusWithin observes focusin/focusout across descendants and retains true when focus moves inside the group. Template refs resolve only on the client; both flags start false during SSR. No initial autofocus is requested. Both stop functions remove their listeners and freeze their recorded flags, so the stopped output is explicitly labelled Last observed. The component scope also disposes both watchers. This does not trap focus or validate the profile fields.

Compare the initial and interacted states
Track and control workshop profile focus: initial state
Initial state
Track and control workshop profile focus: state after the verified interactions
After the verified interactions

Copy the example

Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.

<script setup lang="ts">
import { ref, useId, useTemplateRef } from "vue";
import { useFocus, useFocusWithin } from "@vizejs/composable/focus";

const profileId = useId();
const nameInput = useTemplateRef<HTMLInputElement>("nameInput");
const profile = useTemplateRef<HTMLElement>("profile");
const { focused: nameFocused, stop: stopName } = useFocus(nameInput, { preventScroll: true });
const { focused: profileFocused, stop: stopProfile } = useFocusWithin(profile);
const stopped = ref(false);

function stopTracking(): void {
  stopName();
  stopProfile();
  stopped.value = true;
}
</script>

<template>
  <div class="composable-example">
    <div ref="profile" role="group" aria-label="Workshop profile">
      <label :for="`${profileId}-name`">Badge name</label>
      <input
        :id="`${profileId}-name`"
        ref="nameInput"
        autocomplete="nickname"
        @keydown.esc="() => (nameFocused = false)"
      />
      <label :for="`${profileId}-email`">Workshop contact</label>
      <input :id="`${profileId}-email`" type="email" autocomplete="email" />
      <button type="button" :disabled="stopped" @click="stopTracking">Stop focus tracking</button>
    </div>
    <output aria-live="polite"
      >{{ stopped ? "Last observed" : "Current" }}: name focused {{ nameFocused ? "yes" : "no" }} ·
      profile focus {{ profileFocused ? "yes" : "no" }}</output
    >
    <div class="example-actions">
      <button type="button" :disabled="stopped" @click="() => (nameFocused = true)">
        Focus badge name
      </button>
      <button type="button" :disabled="stopped" @click="() => (nameFocused = false)">
        Blur badge name
      </button>
    </div>
    <p>
      {{
        stopped
          ? "Tracking stopped. The last observed flags stay fixed; the fields remain editable."
          : "Tab between the two fields: the individual flag changes while profile focus stays yes. Tab outside the group to clear it, or press Escape in Badge name to blur it."
      }}
    </p>
  </div>
</template>

Runtime contract

Utility Category Stability SSR Hydration Cleanup Targets Host globals Uses
useFocus dom experimental deterministic-fallback stable caller, reactive-scope web, desktop — resolveElement, tryOnScopeDispose
useFocusWithin dom experimental deterministic-fallback stable caller, reactive-scope web, desktop — resolveElement, tryOnScopeDispose

API

useFocus

Track and control focus of a single element. Listens for focus/blur on the resolved element and re-syncs when the target changes. Assigning focused calls focus()/blur(). Server renders always report false; initialValue is applied only after the element resolves on the client, so hydration output is unaffected.

function useFocus(target: MaybeElementTarget, options: UseFocusOptions = {}): FocusControls

useFocusWithin

Track whether focus is inside an element (the :focus-within state). Uses bubbling focusin/focusout and checks the relatedTarget so moving focus between descendants never flickers to false. Server renders report false.

function useFocusWithin(target: MaybeElementTarget): FocusWithinControls

Types

UseFocusOptions

Options for useFocus.

Member Type Description
initialValue? boolean Focus the element as soon as it resolves.
focusVisible? boolean Only report focus when the user agent would show a focus ring (:focus-visible).
preventScroll? boolean Avoid scrolling the element into view when focusing programmatically.

FocusControls

Reactive focus state returned by useFocus.

Member Type Description
focused WritableComputedRef<boolean> Whether the element is focused. Assign true/false to focus/blur it.
stop () => void Stop tracking. Idempotent.

FocusWithinControls

Reactive state returned by useFocusWithin.

Member Type Description
focused Readonly<Ref<boolean>> Whether the element or any descendant has focus.
stop () => void Stop tracking. Idempotent.