Modal focus room
Source for this preview
<!-- This scenario is kept as its own example because it exercises a native top-layer stack: modal inertness, nested surfaces, Escape ordering, and focus restoration. The other examples focus on one anchored interaction.--><section id="modal-demo" class="modal-demo" aria-label="Modal focus management example"> <p>Open the focus room to test focus trapping, inert neighbors, and layered Escape dismissal.</p> <floating-tree> <floating-root id="modal-root"> <floating-node node-id="focus-room"> <floating-reference> <button class="outline-button"> Enter focus room <span aria-hidden="true">→</span> </button> </floating-reference> </floating-node> <dialog slot="floating" class="modal-panel" aria-labelledby="modal-heading" > <span class="panel-kicker">FOCUS ROOM / PRIVATE</span> <h3 id="modal-heading">You are inside<br />the focus trap.</h3> <p> Nested surfaces keep their own dismissal step. Escape closes the topmost surface before this room. </p> <div class="modal-nested-actions"> <floating-root id="modal-tooltip-root" interactions="hover focus dismiss" floating-role="tooltip" placement="top" > <floating-node node-id="focus-room-tooltip" parent-id="focus-room"> <floating-reference> <button class="text-button">Show placement hint</button> </floating-reference> </floating-node> <template slot="content"> <div class="tooltip">This tooltip stays inside the dialog.</div> </template> </floating-root> <floating-root id="modal-popover-root" interactions="click dismiss" placement="bottom-start" strategy="fixed" > <floating-node node-id="focus-room-popover" parent-id="focus-room"> <floating-reference> <button class="text-button">Open room details</button> </floating-reference> </floating-node> <template slot="content"> <section class="popover-panel" aria-label="Room details"> <span class="panel-kicker">ROOM DETAILS</span> <strong>Details stay above the dialog.</strong> <p>Escape and outside press dismiss only this panel first.</p> <button class="text-button" data-fup-close>Close details</button> </section> </template> </floating-root> <floating-root id="nested-modal-root" > <floating-node node-id="focus-room-child-dialog" parent-id="focus-room"> <floating-reference> <button class="text-button">Open nested dialog</button> </floating-reference> </floating-node> <dialog slot="floating" class="modal-panel nested-modal-panel" aria-label="Nested dialog" > <span class="panel-kicker">SECOND LAYER</span> <h3>One more<br />focus boundary.</h3> <p>Close this layer to resume the parent dialog.</p> <button class="coral-button" data-fup-close> Return to focus room </button> </dialog> </floating-root> </div> <div class="modal-actions"> <button class="coral-button" data-fup-close>Leave room</button> <span class="modal-hint">ESC closes the top layer</span> </div> </dialog> </floating-root> </floating-tree></section>
<script> import { FloatingRootElement, click, dismiss, flip, offset, shift, } from '@floating-ui-plus/web-components'; import {initializeExample} from './initialize-example';
initializeExample('modal', (scope) => { const floating = FloatingRootElement.query(scope, '#modal-root'); floating.configure({ middleware: [offset(20), shift({padding: 24})], plugins: [ click(), // The backdrop owns the whole viewport. Nested surfaces dismiss first. dismiss({outsidePress: false}), ], }); const tooltip = FloatingRootElement.query( scope, '#modal-tooltip-root', ); const popover = FloatingRootElement.query( scope, '#modal-popover-root', ); const nestedModal = FloatingRootElement.query( scope, '#nested-modal-root', ); tooltip.middleware = [offset(12), flip(), shift({padding: 12})]; popover.middleware = [offset(12), flip(), shift({padding: 18})]; nestedModal.configure({ middleware: [offset(20), shift({padding: 24})], plugins: [ click(), dismiss({outsidePress: false}), ], });
});</script><script setup lang="ts">import { FloatingClose, FloatingContent, FloatingNode, FloatingReference, FloatingRoot, FloatingTree, click, dismiss, flip, focus, hover, offset, role, shift,} from '@floating-ui-plus/vue';import {ref, watch} from 'vue';
const open = ref(false);const tooltipOpen = ref(false);const popoverOpen = ref(false);const nestedOpen = ref(false);
const roomPlugins = [ click(), dismiss({outsidePress: false}),];
const tooltipOptions = { placement: 'top', middleware: [offset(12), flip(), shift({padding: 12})],} as const;const tooltipPlugins = [ hover(), focus(), dismiss(), role({role: 'tooltip'}),];
const popoverOptions = { placement: 'bottom-start', middleware: [offset(12), flip(), shift({padding: 18})],} as const;const popoverPlugins = [ click(), dismiss(),];
const nestedPlugins = [ click(), dismiss({outsidePress: false}),];
watch(open, (next) => { if (next) return; tooltipOpen.value = false; popoverOpen.value = false; nestedOpen.value = false;});</script>
<template> <FloatingTree> <FloatingRoot v-model:open="open" :plugins="roomPlugins" > <FloatingNode id="vue-focus-room"> <section class="vue-modal-demo" aria-label="Modal focus management example" > <p> Open the focus room to test focus trapping, inert neighbors, and layered Escape dismissal. </p> <FloatingReference class="vue-button vue-button-outline"> Enter focus room <span>→</span> </FloatingReference>
<FloatingContent as="dialog" class="modal-panel vue-modal" aria-labelledby="vue-modal-heading" > <span class="panel-kicker">FOCUS ROOM / PRIVATE</span> <h3 id="vue-modal-heading"> You are inside<br />the focus trap. </h3> <p> Nested surfaces keep their own dismissal step. Escape closes the topmost surface before this room. </p>
<div class="modal-nested-actions"> <FloatingRoot v-model:open="tooltipOpen" :options="tooltipOptions" :plugins="tooltipPlugins" > <FloatingNode id="vue-focus-room-tooltip"> <FloatingReference class="text-button"> Show placement hint </FloatingReference> <FloatingContent class="tooltip"> This tooltip stays inside the dialog. </FloatingContent> </FloatingNode> </FloatingRoot>
<FloatingRoot v-model:open="popoverOpen" :options="popoverOptions" :plugins="popoverPlugins" > <FloatingNode id="vue-focus-room-popover"> <FloatingReference class="text-button"> Open room details </FloatingReference> <FloatingContent as="section" class="popover-panel" aria-label="Room details" > <span class="panel-kicker">NATIVE POPOVER</span> <strong> Details stay above the dialog. </strong> <p> Escape and outside press dismiss only this panel first. </p> <FloatingClose class="text-button"> Close details </FloatingClose> </FloatingContent> </FloatingNode> </FloatingRoot>
<FloatingRoot v-model:open="nestedOpen" :plugins="nestedPlugins" > <FloatingNode id="vue-focus-room-child-dialog"> <FloatingReference class="text-button"> Open nested dialog </FloatingReference> <FloatingContent as="dialog" class="modal-panel nested-modal-panel vue-modal" aria-label="Nested dialog" > <span class="panel-kicker"> SECOND LAYER </span> <h3> One more<br />focus boundary. </h3> <p> Close this layer to resume the parent dialog. </p> <FloatingClose class="vue-button"> Return to focus room </FloatingClose> </FloatingContent> </FloatingNode> </FloatingRoot> </div>
<div class="modal-actions"> <FloatingClose class="vue-button"> Leave room </FloatingClose> <span class="modal-hint"> ESC closes the top layer </span> </div> </FloatingContent> </section> </FloatingNode> </FloatingRoot> </FloatingTree></template>The focus room is a native <dialog> connected to a floating-node tree. It
contains a tooltip, an anchored popover, and a nested dialog:
- Open the focus room and tab through the trapped controls.
- Open room details; outside press or Escape closes only that panel first.
- Open the nested dialog; its focus boundary becomes the top layer.
- Press Escape repeatedly to close the topmost surface and restore focus to its parent.
<floating-root> <floating-node node-id="focus-room"> <floating-reference><button>Enter focus room</button></floating-reference> </floating-node> <dialog slot="floating" aria-label="Focus room"> <button data-fup-close>Leave room</button> </dialog></floating-root>Use a native dialog when browser-owned modal focus and inertness are desired.
For a custom modal, use FloatingFocusManager/floating-focus-manager, an
overlay, and an explicit portal. The Vue equivalent is
<FloatingContent as="dialog"> with FloatingFocusManager for custom focus
policy. Close requests still pass through onBeforeClose or
floatingbeforeclose.