UI Components Medium
Focus Management
Focus management patterns for single-page applications handling route changes, dynamic content insertion and focus restoration.
Open in Lab
MCP
vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #0a0a0a;
color: #e5e5e5;
min-height: 100vh;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/* โโ Focus Tracker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.focus-tracker {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #111;
border: 1px solid #2a2a2a;
border-radius: 10px;
font-size: 0.78rem;
z-index: 500;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
max-width: 360px;
}
.tracker-label {
color: #666;
font-weight: 600;
flex-shrink: 0;
}
.tracker-element {
color: #22c55e;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.72rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* โโ SPA Navigation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.spa-wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.spa-nav {
display: flex;
align-items: center;
gap: 24px;
padding: 0 28px;
height: 56px;
background: #111;
border-bottom: 1px solid #1e1e1e;
}
.nav-brand {
font-size: 0.95rem;
font-weight: 700;
color: #f5f5f5;
flex-shrink: 0;
}
.nav-links {
display: flex;
gap: 4px;
list-style: none;
}
.spa-link {
padding: 6px 14px;
border-radius: 7px;
font-size: 0.82rem;
font-weight: 500;
color: #777;
text-decoration: none;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.spa-link:hover {
background: #1a1a1a;
color: #ccc;
}
.spa-link.active {
background: #1e1e2e;
color: #e5e5e5;
}
.spa-link:focus-visible {
outline: 2px solid #6366f1;
outline-offset: -2px;
}
/* โโ SPA Main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.spa-main {
flex: 1;
padding: 32px 40px;
max-width: 780px;
margin: 0 auto;
width: 100%;
}
.spa-page {
display: none;
}
.spa-page.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* โโ Pattern Info โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.pattern-info {
background: #111827;
border: 1px solid #1e3a5f;
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 24px;
}
.pattern-badge {
display: inline-block;
padding: 3px 10px;
background: #1e1e2e;
border: 1px solid #3730a3;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 600;
color: #a5b4fc;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 10px;
}
.pattern-info h2 {
font-size: 1rem;
font-weight: 600;
color: #93c5fd;
margin-bottom: 6px;
}
.pattern-info p {
font-size: 0.85rem;
color: #94a3b8;
line-height: 1.6;
}
/* โโ Page Heading โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.page-heading {
font-size: 1.5rem;
font-weight: 700;
color: #f5f5f5;
margin-bottom: 12px;
}
.page-heading:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 4px;
border-radius: 4px;
}
.page-content {
font-size: 0.9rem;
color: #888;
line-height: 1.6;
margin-bottom: 24px;
}
/* โโ Content Cards (Route Change) โโโโโโโโโโโโโโโโโโโ */
.page-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.content-card {
background: #111;
border: 1px solid #222;
border-radius: 12px;
padding: 20px;
}
.content-card h3 {
font-size: 0.95rem;
font-weight: 600;
color: #d4d4d4;
margin-bottom: 6px;
}
.content-card p {
font-size: 0.82rem;
color: #777;
line-height: 1.5;
margin-bottom: 12px;
}
.card-link {
font-size: 0.82rem;
color: #6366f1;
text-decoration: none;
font-weight: 500;
}
.card-link:hover {
text-decoration: underline;
}
/* โโ List Items (Dynamic Content) โโโโโโโโโโโโโโโโโโโ */
.items-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
}
.list-item {
display: flex;
align-items: center;
padding: 12px 16px;
background: #111;
border: 1px solid #222;
border-radius: 8px;
font-size: 0.85rem;
color: #ccc;
transition: border-color 0.15s;
}
.list-item:focus-visible {
outline: 2px solid #6366f1;
outline-offset: -2px;
}
.list-item.new-item {
border-color: #22c55e;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* โโ Task Items (Delete Recovery) โโโโโโโโโโโโโโโโโโโ */
.task-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
}
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #111;
border: 1px solid #222;
border-radius: 8px;
transition: border-color 0.15s, opacity 0.2s;
}
.task-item:focus-visible {
outline: 2px solid #6366f1;
outline-offset: -2px;
}
.task-text {
font-size: 0.85rem;
color: #ccc;
}
.delete-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: #555;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.delete-btn:hover {
background: #1e1e1e;
border-color: #333;
color: #ef4444;
}
.delete-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: -2px;
}
/* โโ Settings Grid (Modal Restore) โโโโโโโโโโโโโโโโโโ */
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.setting-card {
background: #111;
border: 1px solid #222;
border-radius: 12px;
padding: 20px;
}
.setting-card h3 {
font-size: 0.95rem;
font-weight: 600;
color: #d4d4d4;
margin-bottom: 6px;
}
.setting-card p {
font-size: 0.82rem;
color: #777;
line-height: 1.5;
margin-bottom: 16px;
}
/* โโ Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.action-btn {
padding: 10px 20px;
background: #1e1e2e;
border: 1px solid #3730a3;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
color: #a5b4fc;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.action-btn:hover {
background: #2d2d5e;
border-color: #6366f1;
}
.action-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.hint-text {
margin-top: 10px;
font-size: 0.8rem;
color: #555;
font-style: italic;
}
/* โโ Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.modal-backdrop.open {
opacity: 1;
visibility: visible;
}
.modal {
width: 90%;
max-width: 440px;
background: #141414;
border: 1px solid #2a2a2a;
border-radius: 14px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
transform: scale(0.96) translateY(8px);
transition: transform 0.2s;
}
.modal-backdrop.open .modal {
transform: scale(1) translateY(0);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 16px;
border-bottom: 1px solid #222;
}
.modal-title {
font-size: 1.05rem;
font-weight: 700;
color: #f5f5f5;
}
.modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: #666;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.modal-close:hover {
background: #222;
color: #ccc;
}
.modal-close:focus-visible {
outline: 2px solid #6366f1;
outline-offset: -2px;
}
.modal-body {
padding: 20px 24px;
font-size: 0.88rem;
color: #aaa;
line-height: 1.6;
}
.modal-body label {
display: block;
font-size: 0.82rem;
font-weight: 500;
color: #999;
margin-bottom: 6px;
margin-top: 14px;
}
.modal-body label:first-child {
margin-top: 0;
}
.modal-body input {
width: 100%;
padding: 9px 14px;
background: #0a0a0a;
border: 1px solid #262626;
border-radius: 8px;
font-size: 0.85rem;
color: #e5e5e5;
}
.modal-body input:focus {
outline: none;
border-color: #6366f1;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 24px 20px;
border-top: 1px solid #222;
}
.btn-secondary {
padding: 8px 18px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 500;
color: #aaa;
cursor: pointer;
transition: background 0.15s;
}
.btn-secondary:hover {
background: #222;
}
.btn-secondary:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.btn-primary {
padding: 8px 18px;
background: #4f46e5;
border: none;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 600;
color: #fff;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover {
background: #6366f1;
}
.btn-primary:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* โโ Responsive โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
@media (max-width: 640px) {
.spa-main {
padding: 24px 20px;
}
.page-cards {
grid-template-columns: 1fr;
}
.settings-grid {
grid-template-columns: 1fr;
}
.spa-nav {
padding: 0 16px;
gap: 12px;
}
.nav-links {
overflow-x: auto;
}
}(() => {
const liveRegion = document.getElementById("liveRegion");
const trackerElement = document.getElementById("trackerElement");
const spaMain = document.getElementById("spaMain");
/* โโ Focus Tracker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
function describeFocusedElement(el) {
if (!el || el === document.body) return "body";
const tag = el.tagName.toLowerCase();
const role = el.getAttribute("role");
const label = el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 40) || "";
const id = el.id ? `#${el.id}` : "";
const cls = el.className ? `.${el.className.split(" ")[0]}` : "";
const display = role ? `[${role}]` : `<${tag}>`;
return `${display}${id || cls} "${label}"`;
}
document.addEventListener("focusin", (e) => {
trackerElement.textContent = describeFocusedElement(e.target);
});
/* โโ SPA Navigation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const navLinks = document.querySelectorAll(".spa-link");
const pages = document.querySelectorAll(".spa-page");
const pageHeadings = {
"route-change": "Home Page",
"dynamic-content": "Dynamic Content",
"delete-recovery": "Manage Tasks",
"modal-restore": "Settings",
};
function navigateToPage(pageId) {
// Update nav links
navLinks.forEach((link) => {
link.classList.toggle("active", link.dataset.page === pageId);
});
// Update pages
pages.forEach((page) => {
page.classList.toggle("active", page.id === `page-${pageId}`);
});
// Pattern 1: Focus moves to the new page heading
const heading = document.getElementById(`heading-${pageId}`);
if (heading) {
heading.textContent = pageHeadings[pageId] || pageId;
// Small delay to allow DOM update
requestAnimationFrame(() => {
heading.focus();
announce(`Navigated to ${pageHeadings[pageId] || pageId}`);
});
}
}
navLinks.forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
navigateToPage(link.dataset.page);
});
});
/* โโ Live Region Announcements โโโโโโโโโโโโโโโโโโโโ */
function announce(message) {
liveRegion.textContent = "";
requestAnimationFrame(() => {
liveRegion.textContent = message;
});
}
/* โโ Pattern 2: Dynamic Content โโโโโโโโโโโโโโโโโโโ */
const dynamicList = document.getElementById("dynamicList");
const loadMoreBtn = document.getElementById("loadMoreBtn");
let itemCounter = 3;
const sampleContent = [
"Newly fetched article from API",
"User comment just posted",
"System notification received",
"Search result loaded dynamically",
"Feed item from live stream",
];
if (loadMoreBtn) {
loadMoreBtn.addEventListener("click", () => {
// Add 2 new items
const fragment = document.createDocumentFragment();
let firstNew = null;
for (let i = 0; i < 2; i++) {
itemCounter++;
const item = document.createElement("div");
item.className = "list-item new-item";
item.setAttribute("tabindex", "-1");
const text = document.createElement("span");
text.className = "item-text";
const content = sampleContent[(itemCounter - 1) % sampleContent.length];
text.textContent = `Item ${itemCounter} \u2014 ${content}`;
item.appendChild(text);
fragment.appendChild(item);
if (!firstNew) firstNew = item;
}
dynamicList.appendChild(fragment);
// Focus moves to first new item
if (firstNew) {
requestAnimationFrame(() => {
firstNew.focus();
announce(`${2} new items loaded. Item ${itemCounter - 1} and Item ${itemCounter}.`);
});
}
// Remove "new" highlight after animation
setTimeout(() => {
dynamicList.querySelectorAll(".new-item").forEach((el) => {
el.classList.remove("new-item");
});
}, 1500);
});
}
/* โโ Pattern 3: Delete Recovery โโโโโโโโโโโโโโโโโโโ */
const taskList = document.getElementById("taskList");
if (taskList) {
taskList.addEventListener("click", (e) => {
const deleteBtn = e.target.closest(".delete-btn");
if (!deleteBtn) return;
const taskItem = deleteBtn.closest(".task-item");
if (!taskItem) return;
const siblings = [...taskList.querySelectorAll(".task-item")];
const index = siblings.indexOf(taskItem);
const taskName = taskItem.querySelector(".task-text")?.textContent || "Task";
// Remove the item
taskItem.remove();
// Focus recovery: next sibling, or previous, or parent
const remaining = [...taskList.querySelectorAll(".task-item")];
if (remaining.length === 0) {
// No items left, focus the heading
const heading = document.getElementById("heading-delete-recovery");
if (heading) heading.focus();
announce(`Deleted ${taskName}. No tasks remaining.`);
} else if (index < remaining.length) {
// Focus next item (same index position)
remaining[index].focus();
announce(`Deleted ${taskName}. Focus moved to next task.`);
} else {
// Focus previous item (was last)
remaining[remaining.length - 1].focus();
announce(`Deleted ${taskName}. Focus moved to previous task.`);
}
});
}
/* โโ Pattern 4: Modal Focus Restoration โโโโโโโโโโโ */
const modalBackdrop = document.getElementById("modalBackdrop");
const modal = document.getElementById("modal");
const modalTitle = document.getElementById("modalTitle");
const modalBody = document.getElementById("modalBody");
const modalClose = document.getElementById("modalClose");
const modalCancel = document.getElementById("modalCancel");
const modalSave = document.getElementById("modalSave");
let modalTrigger = null;
const modalContent = {
profile: {
title: "Edit Profile",
body: `
<label for="profileName">Display Name</label>
<input type="text" id="profileName" value="Jane Developer" />
<label for="profileBio">Bio</label>
<input type="text" id="profileBio" value="Full-stack engineer" />
`,
},
notifications: {
title: "Notification Preferences",
body: `
<label for="notifEmail">Email notifications</label>
<input type="text" id="notifEmail" value="Enabled" />
<label for="notifPush">Push notifications</label>
<input type="text" id="notifPush" value="Disabled" />
`,
},
security: {
title: "Security Settings",
body: `
<label for="secPassword">Password</label>
<input type="password" id="secPassword" value="placeholder" />
<label for="sec2fa">Two-Factor Auth</label>
<input type="text" id="sec2fa" value="Enabled (Authenticator)" />
`,
},
};
function openModal(type) {
const content = modalContent[type];
if (!content) return;
modalTrigger = document.activeElement;
modalTitle.textContent = content.title;
modalBody.innerHTML = content.body;
modalBackdrop.classList.add("open");
modalBackdrop.setAttribute("aria-hidden", "false");
// Focus first focusable element in modal
requestAnimationFrame(() => {
const firstInput = modal.querySelector("input, button");
if (firstInput) firstInput.focus();
});
announce(`${content.title} dialog opened.`);
}
function closeModal() {
modalBackdrop.classList.remove("open");
modalBackdrop.setAttribute("aria-hidden", "true");
// Pattern 4: Restore focus to the trigger element
if (modalTrigger && modalTrigger.focus) {
requestAnimationFrame(() => {
modalTrigger.focus();
announce("Dialog closed. Focus restored.");
});
}
modalTrigger = null;
}
// Modal triggers
document.querySelectorAll(".modal-trigger").forEach((btn) => {
btn.addEventListener("click", () => {
openModal(btn.dataset.modal);
});
});
// Modal close actions
if (modalClose) modalClose.addEventListener("click", closeModal);
if (modalCancel) modalCancel.addEventListener("click", closeModal);
if (modalSave) {
modalSave.addEventListener("click", () => {
announce("Changes saved.");
closeModal();
});
}
// Backdrop click
if (modalBackdrop) {
modalBackdrop.addEventListener("click", (e) => {
if (e.target === modalBackdrop) closeModal();
});
}
// Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modalBackdrop.classList.contains("open")) {
e.preventDefault();
closeModal();
}
});
// Focus trap in modal
if (modal) {
modal.addEventListener("keydown", (e) => {
if (e.key !== "Tab") return;
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Focus Management</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="spa-wrapper">
<!-- Focus Tracker -->
<div class="focus-tracker" id="focusTracker" aria-hidden="true">
<span class="tracker-label">Focus:</span>
<span class="tracker-element" id="trackerElement">none</span>
</div>
<!-- Live region for announcements -->
<div class="sr-only" aria-live="assertive" id="liveRegion"></div>
<!-- SPA Navigation -->
<nav class="spa-nav" aria-label="Main navigation">
<div class="nav-brand">FocusDemo</div>
<ul class="nav-links">
<li><a href="#" class="spa-link active" data-page="route-change">Route Change</a></li>
<li><a href="#" class="spa-link" data-page="dynamic-content">Dynamic Content</a></li>
<li><a href="#" class="spa-link" data-page="delete-recovery">Delete Recovery</a></li>
<li><a href="#" class="spa-link" data-page="modal-restore">Modal Restore</a></li>
</ul>
</nav>
<!-- Main Content Area -->
<main class="spa-main" id="spaMain">
<!-- Page 1: Route Change -->
<div class="spa-page active" id="page-route-change">
<div class="pattern-info">
<span class="pattern-badge">Pattern 1</span>
<h2>Route Change Focus</h2>
<p>When navigating to a new "page" in an SPA, focus should move to the new page's heading so screen reader users know the page has changed.</p>
</div>
<h1 class="page-heading" tabindex="-1" id="heading-route-change">Home Page</h1>
<p class="page-content">This is the home page. Click any nav link above to simulate a route change. Focus will automatically move to the new page heading.</p>
<div class="page-cards">
<div class="content-card">
<h3>Getting Started</h3>
<p>Learn the fundamentals of accessible route transitions in single-page applications.</p>
<a href="#" class="card-link">Read more</a>
</div>
<div class="content-card">
<h3>Best Practices</h3>
<p>Follow established patterns for managing focus during client-side navigation.</p>
<a href="#" class="card-link">Read more</a>
</div>
</div>
</div>
<!-- Page 2: Dynamic Content -->
<div class="spa-page" id="page-dynamic-content">
<div class="pattern-info">
<span class="pattern-badge">Pattern 2</span>
<h2>Dynamic Content Insertion</h2>
<p>When new content is dynamically added, focus should move to the new content or announce it via a live region so users are aware of the change.</p>
</div>
<h1 class="page-heading" tabindex="-1" id="heading-dynamic-content">Dynamic Content</h1>
<div class="items-list" id="dynamicList">
<div class="list-item" tabindex="-1">
<span class="item-text">Item 1 — Already loaded content</span>
</div>
<div class="list-item" tabindex="-1">
<span class="item-text">Item 2 — Already loaded content</span>
</div>
<div class="list-item" tabindex="-1">
<span class="item-text">Item 3 — Already loaded content</span>
</div>
</div>
<button class="action-btn" id="loadMoreBtn">Load More Items</button>
<p class="hint-text">New items will be announced and focused when loaded.</p>
</div>
<!-- Page 3: Delete Recovery -->
<div class="spa-page" id="page-delete-recovery">
<div class="pattern-info">
<span class="pattern-badge">Pattern 3</span>
<h2>Item Deletion Focus Recovery</h2>
<p>When an item is deleted, focus should move to the next item, the previous item, or a parent container — never lost to the document body.</p>
</div>
<h1 class="page-heading" tabindex="-1" id="heading-delete-recovery">Manage Tasks</h1>
<div class="task-list" id="taskList">
<div class="task-item" tabindex="-1" data-task-id="1">
<span class="task-text">Review pull request #142</span>
<button class="delete-btn" aria-label="Delete task: Review pull request #142">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="task-item" tabindex="-1" data-task-id="2">
<span class="task-text">Update documentation for v2.0</span>
<button class="delete-btn" aria-label="Delete task: Update documentation for v2.0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="task-item" tabindex="-1" data-task-id="3">
<span class="task-text">Fix accessibility issues in nav</span>
<button class="delete-btn" aria-label="Delete task: Fix accessibility issues in nav">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="task-item" tabindex="-1" data-task-id="4">
<span class="task-text">Deploy staging environment</span>
<button class="delete-btn" aria-label="Delete task: Deploy staging environment">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="task-item" tabindex="-1" data-task-id="5">
<span class="task-text">Write unit tests for auth module</span>
<button class="delete-btn" aria-label="Delete task: Write unit tests for auth module">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</div>
<p class="hint-text">Delete items and watch focus move to the nearest remaining sibling.</p>
</div>
<!-- Page 4: Modal Restore -->
<div class="spa-page" id="page-modal-restore">
<div class="pattern-info">
<span class="pattern-badge">Pattern 4</span>
<h2>Modal Focus Restoration</h2>
<p>When a modal/dialog closes, focus must return to the element that triggered it. This prevents focus from being lost.</p>
</div>
<h1 class="page-heading" tabindex="-1" id="heading-modal-restore">Settings</h1>
<div class="settings-grid">
<div class="setting-card">
<h3>Profile</h3>
<p>Update your name, avatar, and bio.</p>
<button class="action-btn modal-trigger" data-modal="profile">Edit Profile</button>
</div>
<div class="setting-card">
<h3>Notifications</h3>
<p>Configure email and push notification preferences.</p>
<button class="action-btn modal-trigger" data-modal="notifications">Configure</button>
</div>
<div class="setting-card">
<h3>Security</h3>
<p>Manage passwords, two-factor auth, and sessions.</p>
<button class="action-btn modal-trigger" data-modal="security">Manage</button>
</div>
</div>
</div>
</main>
</div>
<!-- Modal Template -->
<div class="modal-backdrop" id="modalBackdrop" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" id="modal" aria-labelledby="modalTitle">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle">Dialog</h2>
<button class="modal-close" id="modalClose" aria-label="Close dialog">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body" id="modalBody">
<p>Modal content goes here.</p>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="modalCancel">Cancel</button>
<button class="btn-primary" id="modalSave">Save Changes</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Four essential focus management patterns for single-page applications: route change focus, dynamic content insertion, item deletion recovery, and modal focus restoration. Each pattern is demonstrated interactively with visual focus tracking.