ARIA & Semantics
Floating surfaces are frequently teleported or mounted at the end of the <body> element to escape overflow clipping and stacking contexts.
While this solves CSS layout constraints, it severs the DOM relationship between the trigger button and the floating surface. Sighted users see a dropdown directly underneath its button, but assistive technologies (such as screen readers) have no hierarchical parent-child connection to follow.
To make floating interfaces accessible, you must bridge this gap using standard W3C WAI-ARIA roles, states, and properties.
The semantic contract
A complete floating interface connects the trigger, the surface container, and the internal items through three layers of attributes:
- Trigger state: Informs the user what kind of surface will open (
aria-haspopup), whether it is currently visible (aria-expanded), and the ID of the controlled panel (aria-controls). - Surface container: Declares the purpose of the container (
role="menu",role="listbox",role="dialog",role="tooltip"), whether it is modal (aria-modal="true"), and its accessible name (aria-labelledbyoraria-label). - Internal items: Identifies interactive elements within lists (
role="menuitem",role="option"), along with dynamic states such asaria-disabled,aria-checked, oraria-selected.
Trigger attributes
The anchor button requires attributes that describe the disclosure lifecycle:
| Attribute | Purpose | When to use |
|---|---|---|
aria-haspopup | Signals that activating the element reveals a popup. Values include "menu", "listbox", "tree", "grid", or "dialog". | All popup triggers (menus, select pickers, dialog triggers). |
aria-expanded | Communicates whether the popup is currently visible ("true" or "false"). | All popup triggers except simple tooltips. |
aria-controls | Points to the id of the floating surface element. | Menus, popups, and comboboxes. |
aria-describedby | References an element that provides supplementary descriptive text. | Informational tooltips (role="tooltip"). |
For tooltips, use aria-describedby on the trigger instead of aria-expanded or aria-controls. A tooltip does not open an interactive popup: it describes the anchor.
Surface roles and accessible naming
Every floating surface container needs an explicit role and an accessible name according to the WAI-ARIA Authoring Practices Guide (APG):
- Menus (
role="menu"): For lists of actions or commands (such as "Edit", "Duplicate", "Delete"). - Listboxes (
role="listbox"): For lists of selectable options (such as form selects and autocomplete suggestion pickers). - Dialogs (
role="dialog"): For interactive overlays containing forms, alerts, or multistep workflows. - Tooltips (
role="tooltip"): For non-interactive descriptions that clarify the purpose of an ambiguous control.
Labeling dialogs and surfaces
Screen readers announce a surface container using its accessible name when focus enters.
Provide an accessible name using aria-labelledby pointing to a heading element inside the surface:
<div id="settings-dialog" role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Workspace Settings</h2>
<!-- Dialog content -->
</div>
If the surface has no visible heading, provide a concise name directly via aria-label:
<div id="quick-actions" role="menu" aria-label="Quick actions">
<!-- Menu items -->
</div>
Automating semantics with useRole()
Manually generating unique element IDs and synchronizing attributes across triggers and items adds boilerplate.
VFloat provides useRole to automate this contract. It monitors node.open and dynamically synchronizes ARIA attributes on both elements.
Here is a menu component using useRole:
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
useClick,
useEscapeKey,
useFloatingNode,
useOutsideClick,
usePosition,
useRole,
} from "v-float";
interface ActionItem {
id: string;
label: string;
disabled?: boolean;
}
const items = ref<ActionItem[]>([
{ id: "duplicate", label: "Duplicate" },
{ id: "archive", label: "Archive", disabled: true },
{ id: "delete", label: "Delete" },
]);
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const itemEls = shallowRef<(HTMLElement | null)[]>([]);
const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node, { placement: "bottom-start" });
useClick(node);
useOutsideClick(node);
useEscapeKey(node);
// Synchronize ARIA roles and relationships automatically
useRole(node, {
role: "menu",
listRef: itemEls,
disabledIndices: (index) => !!items.value[index]?.disabled,
});
</script>
<template>
<button ref="anchorEl" type="button">Actions</button>
<ul v-if="node.open.value" ref="floatingEl">
<li
v-for="(item, index) in items"
:key="item.id"
:ref="(el) => (itemEls[index] = el as HTMLElement | null)"
>
{{ item.label }}
</li>
</ul>
</template>
When this component runs, useRole performs the following coordination automatically:
- Generates an auto-incremented ID on the floating list if none exists (using Vue's native
useId). - Applies
aria-haspopup="menu",aria-expanded="false", andaria-controlsto the anchor button. - Flips
aria-expandedto"true"whenevernode.open.valuebecomestrue. - Sets
role="menu"on the floating element. - Traverses
listRefto setrole="menuitem"on every item. - Sets
aria-disabled="true"on items matchingdisabledIndices. - Restores all original DOM attributes when the composable unmounts or disables.
The responsibility boundary
useRole handles dynamic attribute coordination, but your template markup owns static document semantics:
- Semantic HTML elements: Use
<button type="button">for triggers rather than<div @click>. Native buttons provide keyboard activation via Space and Enter for free. - Accessible trigger names: If a button contains only an icon, provide an
aria-labeldirectly on the button element (such asaria-label="More options"). - Visible headings: For dialogs, create a visible header and pass its ID via
labelledByso screen readers announce the dialog title immediately on open.
Official specifications
- W3C WAI-ARIA 1.2 Specification: Canonical technical definitions for roles, states, and properties.
- WAI-ARIA Authoring Practices Guide (APG): Recommended interaction and markup patterns for accessible web components.
Where to go next
- Focus Models: Learn how to manage focus placement, trapping, and return restoration.
- Keyboard Navigation: Implement accessible roving tabindex and virtual focus patterns.
useRoleAPI: Full options, type definitions, and parameter references.