Skip to content
llms.txt
llms.txt

Modal focus room

Open the live modal demo

Live demo

Modal focus room

Source for this preview

ModalExample.astro
<!--
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>

Open the full demo ↗

The focus room is a native <dialog> connected to a floating-node tree. It contains a tooltip, an anchored popover, and a nested dialog:

  1. Open the focus room and tab through the trapped controls.
  2. Open room details; outside press or Escape closes only that panel first.
  3. Open the nested dialog; its focus boundary becomes the top layer.
  4. 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.