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


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