Focus Visible
Uniform focus-visible data attributes driven by the document interaction modality.
| Package | @vizejs/ui/focus-visible |
| Maturity | stable |
| Own the source | vize lib pull focus-visible |
| Requires | context, interaction-modality |
| Aliases | focus ring, focus-visible, keyboard focus indicator |
| Covers | CSS :focus-visible, React Aria FocusRing, WCAG 2.4.7 Focus Visible |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { FocusVisibleProvider } from "@vizejs/ui/focus-visible";
Or copy the source into your project with vize lib pull focus-visible (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Buttons that show a focus ring only for keyboard focus through data-focus-visible. -->
<script setup lang="ts">
import { FocusVisibleProvider } from "@vizejs/ui/focus-visible";
</script>
<template>
<FocusVisibleProvider>
<button type="button">Save</button>
<button type="button">Publish</button>
</FocusVisibleProvider>
</template>
API
FocusVisibleProvider
Source: src/families/accessibility/focus-visible/focus-visible-provider.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
attribute |
string |
"data-focus-visible" |
Attribute set to "true" on the focused descendant while focus should be indicated. |
disabled |
boolean |
false |
Stop publishing modality and focus-visible attributes. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
FocusVisibleSlotState |
Subtree whose focused descendants receive the focus-visible attribute. |
Exposed
| Member | Type | Description |
|---|---|---|
isFocusVisible |
boolean |
Whether a descendant currently shows a focus indicator. |
modality |
FocusVisibleModality | null |
Latest interaction modality, or null before mount and before any input. |
element |
HTMLDivElement | null |
Rendered provider element. |
focusVisibleElement |
Element | null |
Descendant that currently carries the focus-visible attribute. |
Behavior
Normative behavior for @vizejs/ui/focus-visible. focus-visible-provider.vue
(FocusVisibleProvider) reuses the shared document interaction-modality tracker and publishes
uniform data attributes so CSS can style [data-focus-visible] identically across engines,
including engines whose :focus-visible heuristics differ. Every row is proven by the named test.
| State x input | Observable outcome | Proven by |
|---|---|---|
| keyboard interaction, focus moves to a descendant | The descendant gets data-focus-visible="true", the root gets data-vize-modality="keyboard" and data-focus-visible-within. |
keyboard focus marks the focused descendant and publishes the modality |
| focus moves between descendants | The previous descendant loses the attribute and the new one gains it. | keyboard focus marks the focused descendant and publishes the modality |
| pointer press or pointer focus on a non-text control | The mark is removed (or never set) and the root reports data-vize-modality="pointer". |
pointer focus on buttons is not marked and a pointer press clears the mark |
| any modality focusing a text entry field | Text inputs, textareas, and contenteditable hosts are marked, including after touch (data-vize-modality="touch"). |
text entry fields are marked for every modality, touch included |
| focus leaves the subtree | The mark and data-focus-visible-within are removed. |
blur leaving the subtree removes the mark |
attribute / disabled |
The mark uses the configured attribute (renaming moves it); disabled removes every published attribute. |
custom attribute names and disabled providers |
| focus already inside the subtree when mounting | The provider marks the active descendant on mount. | focus already inside the subtree at mount is picked up |
useFocusVisible() |
Inside a provider it reads the provider state; standalone it tracks the document modality; outside a scope it throws VIZE_UI_FOCUS_VISIBLE_SETUP. |
useFocusVisible reads the provider or tracks the document standalone |
| heuristics | Keyboard and virtual focus, text entry, and unknown modality are indicated; pointer and touch on other controls are not. | heuristics mirror browser focus-visible rules |
| SSR | Server markup is byte-identical and carries no modality or focus attributes. | renders byte-identical markup without modality or focus attributes |
| hydration | Hydration emits no warnings even when the document already has a modality; attributes appear after mount. | hydrates without diagnostics even when the document already has a modality |
| public types | Modality, state, and props are closed contracts. | src/families/accessibility/focus-visible/focus-visible.types.test-d.ts |
| DOM/SSR/Vapor | focus-visible-provider.vue compiles in every renderer lane. |
scripts/check-renderers.ts |
| Target | Public contract |
|---|---|
| Root | part="root", data-vize-ui="focus-visible-provider", data-vize-modality, data-focus-visible-within, data-disabled |
| Descendant | data-focus-visible="true" (or the configured attribute) while focus should be indicated |
FocusVisible ships no stylesheet.