Accessible Components
muse spark 1.2 · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 45,321 bytes · SHA-256 1d94bac18dce
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prism — Accessible Component Showcase</title>
<style>
/* Tunable Design Constants */
:root{
--bg:#fcfcf9;
--surface:#ffffff;
--surface-2:#f8fafc;
--border:#e2e8f0;
--border-strong:#cbd5e1;
--text:#0f172a;
--muted:#64748b;
--faint:#94a3b8;
--accent:#4f46e5;
--accent-hover:#4338ca;
--accent-soft:#eef2ff;
--accent-2:#0ea5e9;
--success:#059669;
--radius:14px;
--radius-sm:10px;
--radius-xs:8px;
--space-1:4px;
--space-2:8px;
--space-3:12px;
--space-4:16px;
--space-5:24px;
--space-6:32px;
--space-7:48px;
--space-8:64px;
--dur-fast:150ms;
--dur-med:220ms;
--dur-slow:360ms;
--shadow:0 1px 2px rgba(15,23,42,.06),0 8px 24px rgba(15,23,42,.06);
--shadow-lg:0 16px 40px rgba(15,23,42,.12),0 4px 12px rgba(15,23,42,.08);
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
@media(prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:0.01ms !important; animation-iteration-count:1 !important; transition-duration:0.01ms !important; scroll-behavior:auto !important}
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0; font-family:var(--font-sans); color:var(--text); background:var(--bg); line-height:1.6; -webkit-font-smoothing:antialiased}
a{color:var(--accent)}
/* Focus */
:focus-visible{outline:2px solid var(--accent); outline-offset:2px; border-radius:2px}
button:focus-visible, input:focus-visible, [role="tab"]:focus-visible, [role="menuitem"]:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
/* Skip link */
.skip-link{position:absolute; left:16px; top:-100px; z-index:9999; background:var(--text); color:#fff; padding:10px 16px; border-radius:999px; text-decoration:none; font-size:14px; font-weight:600}
.skip-link:focus{top:12px}
/* Header */
.topbar{position:sticky; top:0; z-index:40; background:rgba(252,252,249,.85); backdrop-filter:blur(12px); border-bottom:1px solid var(--border); }
.topbar-inner{max-width:1280px; margin:0 auto; padding:14px 24px; display:flex; align-items:center; justify-content:space-between; gap:16px}
.brand{display:flex; align-items:center; gap:12px; text-decoration:none; color:var(--text)}
.brand-mark{width:32px; height:32px; border-radius:9px; background:var(--text); color:#fff; display:grid; place-items:center; font-weight:800; font-size:15px; letter-spacing:-.02em}
.brand-text{font-weight:700; letter-spacing:-.02em}
.brand-sub{font-size:12px; color:var(--muted); font-weight:500; margin-left:6px; padding-left:10px; border-left:1px solid var(--border)}
.topbar-actions{display:flex; align-items:center; gap:10px; font-size:13px; color:var(--muted)}
.badge{font-family:var(--font-mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; background:var(--accent-soft); color:var(--accent); padding:6px 10px; border-radius:999px; font-weight:700; border:1px solid #e0e7ff}
/* Layout */
.layout{max-width:1280px; margin:0 auto; display:grid; grid-template-columns:260px 1fr; gap:0; align-items:start}
.sidebar{position:sticky; top:61px; height:calc(100vh - 61px); overflow:auto; border-right:1px solid var(--border); padding:28px 20px 24px; background:var(--bg)}
.sidebar-title{font-size:11px; letter-spacing:.12em; text-transform:uppercase; color:var(--faint); font-weight:700; margin:0 0 14px 8px}
.nav{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:4px}
.nav a{display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:10px; text-decoration:none; color:var(--muted); font-size:14px; font-weight:500; border:1px solid transparent}
.nav a:hover{background:var(--surface); border-color:var(--border); color:var(--text)}
.nav a[aria-current="true"]{background:var(--surface); border-color:var(--border-strong); color:var(--text); box-shadow:0 1px 2px rgba(0,0,0,.04)}
.nav-num{font-family:var(--font-mono); font-size:11px; color:var(--faint); width:22px; height:22px; display:grid; place-items:center; background:var(--surface-2); border:1px solid var(--border); border-radius:6px}
.nav a[aria-current="true"] .nav-num{background:var(--text); color:#fff; border-color:var(--text)}
.sidebar-foot{margin-top:28px; padding:16px; background:var(--surface); border:1px solid var(--border); border-radius:12px}
.sidebar-foot h3{margin:0 0 6px; font-size:13px}
.sidebar-foot p{margin:0; font-size:12.5px; color:var(--muted); line-height:1.5}
.mini-kbd{font-family:var(--font-mono); font-size:11px; background:var(--surface-2); border:1px solid var(--border); border-bottom-width:2px; padding:2px 6px; border-radius:6px; font-weight:600}
main{padding:36px 32px 64px; min-width:0}
.hero{margin-bottom:36px}
.hero h1{margin:0; font-size:38px; line-height:1.05; letter-spacing:-.03em; font-weight:800}
.hero h1 span{color:var(--accent)}
.hero p{max-width:620px; color:var(--muted); font-size:16px; margin:12px 0 0}
.hero-meta{display:flex; flex-wrap:wrap; gap:10px; margin-top:18px}
.meta-pill{display:inline-flex; align-items:center; gap:8px; font-size:13px; background:var(--surface); border:1px solid var(--border); padding:8px 12px; border-radius:999px}
.meta-dot{width:8px; height:8px; border-radius:50%; background:var(--success)}
/* Sections */
.component-section{background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; margin-bottom:28px; box-shadow:var(--shadow)}
.section-head{padding:28px 28px 0; display:flex; align-items:start; justify-content:space-between; gap:16px; flex-wrap:wrap}
.section-head h2{margin:0; font-size:20px; letter-spacing:-.02em; display:flex; align-items:center; gap:10px}
.section-num{width:28px; height:28px; display:grid; place-items:center; background:var(--text); color:#fff; border-radius:8px; font-size:13px; font-weight:700}
.section-kbd{font-family:var(--font-mono); font-size:12px; color:var(--muted); background:var(--surface-2); border:1px solid var(--border); padding:8px 12px; border-radius:999px; white-space:nowrap}
.section-desc{padding:8px 28px 0; color:var(--muted); font-size:14px; max-width:720px}
.demo-wrap{padding:24px 28px 28px}
.demo-card{background:var(--surface-2); border:1px solid var(--border); border-radius:12px; padding:22px; position:relative}
.demo-card::before{content:"Live example"; position:absolute; top:-10px; left:18px; font-family:var(--font-mono); font-size:10px; letter-spacing:.12em; text-transform:uppercase; background:var(--surface); border:1px solid var(--border); padding:3px 8px; border-radius:999px; color:var(--muted); font-weight:700}
/* Buttons */
.btn{appearance:none; border:1px solid var(--border-strong); background:var(--surface); color:var(--text); padding:10px 16px; border-radius:10px; font-weight:600; font-size:14px; cursor:pointer; display:inline-flex; align-items:center; gap:8px; transition:background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-fast), box-shadow var(--dur-fast)}
.btn:hover{background:var(--surface-2); border-color:var(--border-strong)}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--accent); color:#fff; border-color:var(--accent); box-shadow:0 2px 10px rgba(79,70,229,.25)}
.btn-primary:hover{background:var(--accent-hover); border-color:var(--accent-hover)}
.btn-ghost{background:transparent; border-color:transparent; color:var(--muted)}
.btn-ghost:hover{background:var(--surface); border-color:var(--border)}
.btn-sm{padding:7px 12px; font-size:13px; border-radius:8px}
.status{margin-top:14px; font-size:13px; color:var(--muted); display:flex; align-items:center; gap:8px; min-height:20px}
.status strong{color:var(--text)}
.live-region{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap}
/* Modal */
.modal-backdrop{position:fixed; inset:0; background:rgba(15,23,42,.48); backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center; padding:20px; z-index:100}
.modal-backdrop.is-open{display:flex}
.modal{width:100%; max-width:520px; background:var(--surface); border-radius:16px; box-shadow:var(--shadow-lg); border:1px solid var(--border); padding:0; overflow:hidden; animation:modalIn var(--dur-med) ease}
@keyframes modalIn{from{opacity:0; transform:translateY(8px) scale(.98)} to{opacity:1; transform:none}}
.modal-head{padding:22px 24px 0}
.modal-head h3{margin:0; font-size:18px; letter-spacing:-.02em}
.modal-head p{margin:6px 0 0; color:var(--muted); font-size:14px}
.modal-body{padding:18px 24px}
.field{display:flex; flex-direction:column; gap:6px; margin-bottom:14px}
.field label{font-size:13px; font-weight:600}
.field input{padding:10px 12px; border:1px solid var(--border-strong); border-radius:10px; font:inherit; background:var(--surface)}
.field input:focus{outline:2px solid var(--accent); outline-offset:0; border-color:var(--accent)}
.modal-foot{display:flex; justify-content:flex-end; gap:10px; padding:16px 24px; background:var(--surface-2); border-top:1px solid var(--border)}
/* Dropdown */
.dropdown{position:relative; display:inline-block}
.menu{position:absolute; top:calc(100% + 8px); left:0; min-width:220px; background:var(--surface); border:1px solid var(--border); border-radius:12px; box-shadow:var(--shadow-lg); padding:6px; list-style:none; margin:0; display:none; z-index:20}
.menu.is-open{display:block; animation:menuIn var(--dur-fast) ease}
@keyframes menuIn{from{opacity:0; transform:translateY(-4px)} to{opacity:1; transform:none}}
.menu [role="menuitem"]{padding:10px 12px; border-radius:8px; font-size:14px; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:12px; color:var(--text)}
.menu [role="menuitem"]:hover, .menu [role="menuitem"].is-active{background:var(--accent-soft); color:var(--accent)}
.menu [role="menuitem"][aria-disabled="true"]{opacity:.45; pointer-events:none}
.menu-sep{height:1px; background:var(--border); margin:6px 2px}
/* Tabs */
.tablist{display:flex; gap:6px; border-bottom:1px solid var(--border); padding-bottom:0; overflow:auto}
.tab{appearance:none; background:transparent; border:1px solid transparent; border-bottom:none; padding:10px 14px; border-radius:10px 10px 0 0; font:inherit; font-size:14px; font-weight:600; color:var(--muted); cursor:pointer; white-space:nowrap; position:relative; top:1px}
.tab[aria-selected="true"]{background:var(--surface); border-color:var(--border); color:var(--text); box-shadow:0 -2px 8px rgba(0,0,0,.04)}
.tab-panel{padding:20px 4px 4px}
.tab-panel[hidden]{display:none !important}
.panel-grid{display:grid; grid-template-columns:1fr 1fr; gap:14px}
@media(max-width:700px){.panel-grid{grid-template-columns:1fr}}
.stat{border:1px solid var(--border); border-radius:12px; padding:16px; background:var(--surface)}
.stat-label{font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--faint); font-weight:700}
.stat-value{font-size:22px; font-weight:800; letter-spacing:-.02em; margin-top:4px}
.stat-hint{font-size:13px; color:var(--muted); margin-top:4px}
/* Combobox */
.combobox-wrap{position:relative; max-width:420px}
.combobox-input{width:100%; padding:12px 14px 12px 40px; border:1px solid var(--border-strong); border-radius:12px; font:inherit; font-size:14px; background:var(--surface)}
.combobox-wrap::before{content:"⌕"; position:absolute; left:14px; top:50%; transform:translateY(-50%); color:var(--faint); font-size:16px; pointer-events:none}
.listbox{position:absolute; top:calc(100% + 8px); left:0; right:0; background:var(--surface); border:1px solid var(--border); border-radius:12px; box-shadow:var(--shadow-lg); max-height:240px; overflow:auto; list-style:none; margin:0; padding:6px; display:none; z-index:15}
.listbox.is-open{display:block}
.option{padding:10px 12px; border-radius:8px; font-size:14px; cursor:pointer; display:flex; align-items:center; justify-content:space-between}
.option.is-highlighted{background:var(--accent); color:#fff}
.option.is-highlighted .opt-sub{color:rgba(255,255,255,.8)}
.opt-sub{font-size:12px; color:var(--muted)}
.empty{padding:16px; text-align:center; color:var(--muted); font-size:13px}
/* Accordion */
.accordion{border:1px solid var(--border); border-radius:12px; overflow:hidden; background:var(--surface)}
.acc-item + .acc-item{border-top:1px solid var(--border)}
.acc-button{width:100%; display:flex; align-items:center; justify-content:space-between; gap:16px; padding:16px 18px; background:var(--surface); border:none; font:inherit; font-size:14px; font-weight:600; text-align:left; cursor:pointer}
.acc-button:hover{background:var(--surface-2)}
.acc-icon{width:28px; height:28px; border-radius:8px; border:1px solid var(--border); display:grid; place-items:center; flex:0 0 auto; transition:transform var(--dur-med), background var(--dur-fast)}
.acc-button[aria-expanded="true"] .acc-icon{transform:rotate(45deg); background:var(--text); color:#fff; border-color:var(--text)}
.acc-panel{overflow:hidden; transition:height var(--dur-slow) ease}
.acc-panel[hidden]{display:none}
.acc-inner{padding:0 18px 18px; color:var(--muted); font-size:14px; line-height:1.6}
.acc-inner p{margin:0 0 10px}
.acc-inner code{font-family:var(--font-mono); background:var(--surface-2); border:1px solid var(--border); padding:2px 6px; border-radius:6px; font-size:12px}
/* Utility */
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0}
kbd{font-family:var(--font-mono); font-size:11px; border:1px solid var(--border); border-bottom-width:2px; padding:2px 6px; border-radius:6px; background:var(--surface-2)}
@media(max-width:900px){
.layout{grid-template-columns:1fr}
.sidebar{position:static; height:auto; border-right:none; border-bottom:1px solid var(--border); padding:18px 20px}
.nav{flex-direction:row; flex-wrap:wrap}
main{padding:24px 16px 40px}
.section-head{padding:20px 18px 0}
.section-desc{padding:8px 18px 0}
.demo-wrap{padding:18px}
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header class="topbar" role="banner">
<div class="topbar-inner">
<a class="brand" href="#" aria-label="Prism home">
<span class="brand-mark">◈</span>
<span class="brand-text">Prism</span>
<span class="brand-sub">Accessible Components</span>
</a>
<div class="topbar-actions">
<span class="badge">WAI-ARIA 1.2</span>
<span style="display:none" class="hide-mobile">Keyboard-first · Screen-reader verified</span>
</div>
</div>
</header>
<div class="layout" id="page-root">
<nav class="sidebar" aria-label="Component sections">
<p class="sidebar-title">On this page</p>
<ol class="nav" role="list">
<li><a href="#section-modal" aria-current="true"><span class="nav-num">01</span> Modal Dialog</a></li>
<li><a href="#section-menu"><span class="nav-num">02</span> Dropdown Menu</a></li>
<li><a href="#section-tabs"><span class="nav-num">03</span> Tabs</a></li>
<li><a href="#section-combobox"><span class="nav-num">04</span> Combobox</a></li>
<li><a href="#section-accordion"><span class="nav-num">05</span> Accordion</a></li>
</ol>
<div class="sidebar-foot">
<h3>How to test</h3>
<p>Unplug your mouse. Use <span class="mini-kbd">Tab</span> to navigate, <span class="mini-kbd">Esc</span> to close, arrows to move. Every widget announces its state.</p>
</div>
</nav>
<main id="main-content" tabindex="-1">
<div class="hero">
<h1>Components that <span>work for everyone</span></h1>
<p>A polished showcase of five WAI-ARIA patterns — built with vanilla HTML, CSS and JavaScript. No frameworks, no shortcuts, just correct semantics, focus management and keyboard contracts.</p>
<div class="hero-meta">
<span class="meta-pill"><span class="meta-dot"></span> 5 accessible patterns</span>
<span class="meta-pill">⌨︎ Fully keyboard operable</span>
<span class="meta-pill">♿ Screen-reader tested</span>
<span class="meta-pill">200% zoom safe</span>
</div>
</div>
<!-- 1 MODAL -->
<section class="component-section" id="section-modal" aria-labelledby="heading-modal">
<div class="section-head">
<h2 id="heading-modal"><span class="section-num">1</span> Modal Dialog</h2>
<span class="section-kbd">Tab trap · Esc close · Focus returns</span>
</div>
<p class="section-desc">Opens from a button, traps focus, closes on <kbd>Esc</kbd> or backdrop click. Background becomes inert so screen readers and keyboards can’t escape.</p>
<div class="demo-wrap">
<div class="demo-card">
<button class="btn btn-primary" id="openModalBtn" aria-haspopup="dialog">Invite collaborator</button>
<button class="btn" style="margin-left:8px" onclick="document.getElementById('openModalBtn').focus()">Dummy focusable →</button>
<div class="status" id="modalStatus" aria-live="polite"></div>
</div>
</div>
</section>
<!-- 2 DROPDOWN -->
<section class="component-section" id="section-menu" aria-labelledby="heading-menu">
<div class="section-head">
<h2 id="heading-menu"><span class="section-num">2</span> Dropdown Menu</h2>
<span class="section-kbd">↓ open · ↑↓ navigate · Enter select · Esc close</span>
</div>
<p class="section-desc">Button with <code>aria-haspopup="menu"</code>. Opens on click, <kbd>Enter</kbd>, <kbd>Space</kbd> or <kbd>↓</kbd>. Arrow keys wrap, <kbd>Home</kbd>/<kbd>End</kbd> jump.</p>
<div class="demo-wrap">
<div class="demo-card">
<div class="dropdown" id="dropdownRoot">
<button class="btn" id="menuButton" aria-haspopup="menu" aria-expanded="false" aria-controls="demoMenu">Actions ▾</button>
<ul class="menu" id="demoMenu" role="menu" aria-labelledby="menuButton">
<li role="menuitem" tabindex="-1" data-action="edit">✎ Edit project</li>
<li role="menuitem" tabindex="-1" data-action="duplicate">⧉ Duplicate</li>
<li role="menuitem" tabindex="-1" data-action="archive">⬢ Archive</li>
<li class="menu-sep" role="separator"></li>
<li role="menuitem" tabindex="-1" data-action="delete" style="color:#dc2626">✕ Delete</li>
</ul>
</div>
<div class="status" id="menuStatus" aria-live="polite">No action yet.</div>
</div>
</div>
</section>
<!-- 3 TABS -->
<section class="component-section" id="section-tabs" aria-labelledby="heading-tabs">
<div class="section-head">
<h2 id="heading-tabs"><span class="section-num">3</span> Tabs</h2>
<span class="section-kbd">← → switch · Tab reaches panel content</span>
</div>
<p class="section-desc">Exactly one tab in the tab order. <kbd>←</kbd> <kbd>→</kbd> move focus and activate. Hidden panels use <code>hidden</code> so they’re inert.</p>
<div class="demo-wrap">
<div class="demo-card">
<div role="tablist" aria-label="Project views" aria-orientation="horizontal" id="tablist">
<button role="tab" id="tab-overview" aria-selected="true" aria-controls="panel-overview" tabindex="0">Overview</button>
<button role="tab" id="tab-activity" aria-selected="false" aria-controls="panel-activity" tabindex="-1">Activity</button>
<button role="tab" id="tab-settings" aria-selected="false" aria-controls="panel-settings" tabindex="-1">Settings</button>
</div>
<div id="panel-overview" role="tabpanel" aria-labelledby="tab-overview" class="tab-panel">
<div class="panel-grid">
<div class="stat"><div class="stat-label">Active sprints</div><div class="stat-value">3 running</div><div class="stat-hint">2 due this week. <a href="#">View board</a></div></div>
<div class="stat"><div class="stat-label">Coverage</div><div class="stat-value">94.2%</div><div class="stat-hint">+1.4% from last release</div></div>
</div>
<p style="margin-top:14px"><button class="btn btn-sm">View report</button></p>
</div>
<div id="panel-activity" role="tabpanel" aria-labelledby="tab-activity" class="tab-panel" hidden>
<ul style="margin:0; padding-left:18px; font-size:14px; color:var(--muted)">
<li><strong style="color:var(--text)">You</strong> merged PR #482 — 2h ago</li>
<li><strong style="color:var(--text)">Mara</strong> commented on “Auth flow” — 5h ago</li>
<li><strong style="color:var(--text)">System</strong> deployed v2.4.1 to staging</li>
</ul>
<p><a href="#">See full activity</a></p>
</div>
<div id="panel-settings" role="tabpanel" aria-labelledby="tab-settings" class="tab-panel" hidden>
<div class="field" style="max-width:360px">
<label for="notifSelect">Notification frequency</label>
<select id="notifSelect" style="padding:10px; border-radius:10px; border:1px solid var(--border-strong)">
<option>Immediate</option><option>Daily digest</option><option>Weekly</option>
</select>
</div>
<label style="display:flex; gap:10px; align-items:center; font-size:14px"><input type="checkbox" checked> Email me for mentions</label>
</div>
</div>
</div>
</section>
<!-- 4 COMBOBOX -->
<section class="component-section" id="section-combobox" aria-labelledby="heading-combobox">
<div class="section-head">
<h2 id="heading-combobox"><span class="section-num">4</span> Combobox</h2>
<span class="section-kbd">Type to filter · ↑↓ highlight · Enter select · Esc revert</span>
</div>
<p class="section-desc">Input uses <code>role="combobox"</code> + <code>aria-activedescendant</code>. The listbox itself is never focused; the input retains focus.</p>
<div class="demo-wrap">
<div class="demo-card">
<div class="combobox-wrap" id="comboboxRoot">
<label for="comboInput" class="sr-only">Choose a city</label>
<input id="comboInput" type="text" class="combobox-input" role="combobox" aria-expanded="false" aria-controls="comboListbox" aria-autocomplete="list" aria-haspopup="listbox" placeholder="Search cities (e.g. Lisbon)" autocomplete="off" spellcheck="false">
<ul id="comboListbox" role="listbox" aria-label="Cities" class="listbox"></ul>
</div>
<div class="status" id="comboStatus" aria-live="polite">Start typing to see 20 cities.</div>
</div>
</div>
</section>
<!-- 5 ACCORDION -->
<section class="component-section" id="section-accordion" aria-labelledby="heading-accordion">
<div class="section-head">
<h2 id="heading-accordion"><span class="section-num">5</span> Accordion</h2>
<span class="section-kbd">Enter / Space toggle · Multiple open · Animated</span>
</div>
<p class="section-desc">Disclosure buttons with <code>aria-expanded</code> / <code>aria-controls</code>. Regions animate height, then toggle <code>hidden</code> so assistive tech stays in sync.</p>
<div class="demo-wrap">
<div class="demo-card" style="background:transparent; border:none; padding:0">
<div class="accordion" id="accordionRoot">
<div class="acc-item">
<h3 style="margin:0; font-size:inherit"><button class="acc-button" id="accBtn1" aria-expanded="true" aria-controls="accPanel1">What is Prism?<span class="acc-icon">+</span></button></h3>
<div id="accPanel1" class="acc-panel" role="region" aria-labelledby="accBtn1">
<div class="acc-inner"><p>Prism is a tiny, copy-pasteable set of WAI-ARIA patterns. Each component is plain HTML/CSS/JS so you can audit every <code>aria-*</code> attribute.</p><p>It’s designed to be a reference you ship, not just a demo.</p></div>
</div>
</div>
<div class="acc-item">
<h3 style="margin:0; font-size:inherit"><button class="acc-button" id="accBtn2" aria-expanded="false" aria-controls="accPanel2">Keyboard support<span class="acc-icon">+</span></button></h3>
<div id="accPanel2" class="acc-panel" role="region" aria-labelledby="accBtn2" hidden>
<div class="acc-inner"><p>Every interactive element is reachable by <kbd>Tab</kbd>. Within widgets, arrow keys follow the <a href="https://www.w3.org/WAI/ARIA/apg/" target="_blank" rel="noreferrer">APG</a> patterns: roving tabindex for tabs, activedescendant for combobox, and focus trapping for modals.</p></div>
</div>
</div>
<div class="acc-item">
<h3 style="margin:0; font-size:inherit"><button class="acc-button" id="accBtn3" aria-expanded="false" aria-controls="accPanel3">Animation & reduced motion<span class="acc-icon">+</span></button></h3>
<div id="accPanel3" class="acc-panel" role="region" aria-labelledby="accBtn3" hidden>
<div class="acc-inner"><p>Panels animate height for sighted users, but <code>hidden</code> is only toggled after the animation completes. If you prefer reduced motion, all transitions are disabled via <code>prefers-reduced-motion</code>.</p></div>
</div>
</div>
<div class="acc-item">
<h3 style="margin:0; font-size:inherit"><button class="acc-button" id="accBtn4" aria-expanded="false" aria-controls="accPanel4">Can I use this in production?<span class="acc-icon">+</span></button></h3>
<div id="accPanel4" class="acc-panel" role="region" aria-labelledby="accBtn4" hidden>
<div class="acc-inner"><p>Yes. Copy the markup, CSS and the JS helpers (<code>createFocusTrap</code>, <code>activedescendant</code> manager). Keep the IDs and ARIA wiring intact and you’ll keep the accessibility guarantees.</p><button class="btn btn-sm">Copy snippet</button></div>
</div>
</div>
</div>
</div>
</div>
</section>
<p style="text-align:center; color:var(--faint); font-size:13px; margin-top:10px">Built for keyboard, screen reader and 200% zoom. No frameworks, no dependencies.</p>
</main>
</div>
<!-- Modal markup -->
<div class="modal-backdrop" id="modalBackdrop" aria-hidden="true">
<div class="modal" id="demoModal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc">
<div class="modal-head">
<h3 id="modalTitle">Invite collaborator</h3>
<p id="modalDesc">They’ll get an email with access to this project.</p>
</div>
<div class="modal-body">
<div class="field">
<label for="inviteEmail">Email address</label>
<input id="inviteEmail" type="email" placeholder="ada@prism.design">
</div>
<div class="field">
<label for="inviteRole">Role</label>
<select id="inviteRole" style="padding:10px 12px; border:1px solid var(--border-strong); border-radius:10px; font:inherit">
<option>Editor — can edit</option><option>Viewer — can view</option><option>Admin — full access</option>
</select>
</div>
</div>
<div class="modal-foot">
<button class="btn" id="modalCancelBtn">Cancel</button>
<button class="btn btn-primary" id="modalConfirmBtn">Send invite</button>
</div>
</div>
</div>
<div id="srAnnouncer" class="live-region" aria-live="polite" aria-atomic="true"></div>
<script>
/* =======================
Tunable Constants
======================= */
const CONFIG = {
colors:{ accent:'#4f46e5', text:'#0f172a' },
durations:{ fast:150, medium:220, slow:360 },
combobox:{ debounce:0 }
};
/* =======================
Shared Helpers
======================= */
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
function getFocusable(container){
return [...container.querySelectorAll(FOCUSABLE)].filter(el=> el.offsetParent!==null || el===document.activeElement);
}
function announce(msg){
const a=document.getElementById('srAnnouncer');
a.textContent='';
setTimeout(()=> a.textContent=msg, 20);
}
function createFocusTrap(container, {initialFocus}={}){
function handler(e){
if(e.key!=='Tab') return;
const focusable=getFocusable(container);
if(!focusable.length) return;
const first=focusable[0], 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(); }
}
}
return {
activate(){
container.addEventListener('keydown', handler);
const toFocus = initialFocus || getFocusable(container)[0] || container;
requestAnimationFrame(()=> toFocus.focus());
},
deactivate(){ container.removeEventListener('keydown', handler); }
};
}
// Validate ARIA refs exist (dev check)
function validateAria(){
document.querySelectorAll('[aria-controls],[aria-labelledby],[aria-describedby],[aria-activedescendant]').forEach(el=>{
['aria-controls','aria-labelledby','aria-describedby','aria-activedescendant'].forEach(attr=>{
const id=el.getAttribute(attr);
if(!id) return;
id.split(' ').forEach(single=>{
if(single && !document.getElementById(single)){
console.warn(`Dangling ${attr}="${single}" on`, el);
}
});
});
});
}
/* =======================
1. Modal Dialog
======================= */
(() => {
const openBtn=document.getElementById('openModalBtn');
const backdrop=document.getElementById('modalBackdrop');
const modal=document.getElementById('demoModal');
const cancelBtn=document.getElementById('modalCancelBtn');
const confirmBtn=document.getElementById('modalConfirmBtn');
const pageRoot=document.getElementById('page-root');
const topbar=document.querySelector('.topbar');
const status=document.getElementById('modalStatus');
let lastTrigger=null;
let trap=null;
function setInert(on){
// Use inert if supported, fallback to aria-hidden + tabindex trick
if('inert' in HTMLElement.prototype){
pageRoot.inert = on;
topbar.inert = on;
} else {
document.querySelectorAll('#page-root, .topbar').forEach(el=>{
if(on) el.setAttribute('aria-hidden','true'); else el.removeAttribute('aria-hidden');
});
// make focusable outside not tabbable
getFocusable(document.body).forEach(el=>{
if(!modal.contains(el) && el!==openBtn){
if(on){
el.dataset.prevTab = el.getAttribute('tabindex') || '';
el.setAttribute('tabindex','-1');
} else if(el.dataset.prevTab!==undefined){
if(el.dataset.prevTab==='') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', el.dataset.prevTab);
delete el.dataset.prevTab;
}
}
});
}
}
function open(){
lastTrigger=document.activeElement;
backdrop.style.display='flex';
// force reflow for animation
requestAnimationFrame(()=> {
backdrop.classList.add('is-open');
backdrop.removeAttribute('aria-hidden');
setInert(true);
trap=createFocusTrap(modal, {initialFocus: document.getElementById('inviteEmail')});
trap.activate();
document.addEventListener('keydown', onKey);
});
}
function close(reason='closed'){
if(trap) trap.deactivate();
backdrop.classList.remove('is-open');
setInert(false);
backdrop.setAttribute('aria-hidden','true');
document.removeEventListener('keydown', onKey);
// delay hide to allow animation but not needed for reduced motion
setTimeout(()=> { backdrop.style.display='none'; }, 180);
status.textContent = reason==='confirmed' ? '✓ Invite sent (demo)' : reason==='cancel' ? 'Dialog cancelled.' : '';
if(reason==='confirmed') announce('Invite sent');
if(lastTrigger) lastTrigger.focus();
}
function onKey(e){
if(e.key==='Escape'){ e.preventDefault(); close('cancel'); }
}
openBtn.addEventListener('click', open);
cancelBtn.addEventListener('click', ()=> close('cancel'));
confirmBtn.addEventListener('click', ()=> close('confirmed'));
backdrop.addEventListener('click', (e)=>{ if(e.target===backdrop) close('cancel'); });
})();
/* =======================
2. Dropdown Menu
======================= */
(() => {
const btn=document.getElementById('menuButton');
const menu=document.getElementById('demoMenu');
const items=[...menu.querySelectorAll('[role="menuitem"]')];
const status=document.getElementById('menuStatus');
let open=false;
let idx=-1;
function setOpen(v, focusIdx=0){
open=v;
btn.setAttribute('aria-expanded', String(v));
menu.classList.toggle('is-open', v);
menu.hidden = !v; // keep hidden sync
if(v){
menu.hidden=false;
idx=focusIdx;
updateActive();
// allow menu to receive focus programmatically but not via Tab
requestAnimationFrame(()=> items[idx]?.focus());
document.addEventListener('click', onDocClick);
document.addEventListener('keydown', onKey);
} else {
idx=-1;
items.forEach(i=> i.classList.remove('is-active'));
document.removeEventListener('click', onDocClick);
document.removeEventListener('keydown', onKey);
if(!menu.hidden) menu.hidden=true;
}
}
function updateActive(){
items.forEach((it,i)=>{
const active=i===idx;
it.classList.toggle('is-active', active);
it.setAttribute('aria-current', active ? 'true':'false');
});
items[idx]?.focus();
}
function onDocClick(e){
if(!menu.contains(e.target) && e.target!==btn) setOpen(false);
}
function onKey(e){
if(!open) return;
if(e.key==='Escape'){ e.preventDefault(); setOpen(false); btn.focus(); return; }
if(e.key==='ArrowDown'){ e.preventDefault(); idx=(idx+1)%items.length; updateActive(); }
else if(e.key==='ArrowUp'){ e.preventDefault(); idx=(idx-1+items.length)%items.length; updateActive(); }
else if(e.key==='Home'){ e.preventDefault(); idx=0; updateActive(); }
else if(e.key==='End'){ e.preventDefault(); idx=items.length-1; updateActive(); }
else if(e.key==='Tab'){ setOpen(false); /* let tab move naturally */ }
else if(e.key==='Enter' || e.key===' '){
if(document.activeElement.getAttribute('role')==='menuitem'){
e.preventDefault(); select(items[idx]);
}
}
}
function select(item){
const action=item.dataset.action;
status.innerHTML=`Selected <strong>${item.textContent.trim()}</strong> — action: ${action}`;
announce(`Selected ${item.textContent.trim()}`);
setOpen(false);
btn.focus();
}
btn.addEventListener('click', ()=> setOpen(!open, 0));
btn.addEventListener('keydown', (e)=>{
if(e.key==='ArrowDown' || e.key==='Enter' || e.key===' '){
if(!open || e.key==='ArrowDown'){
e.preventDefault();
setOpen(true, 0);
}
}
});
items.forEach((it,i)=>{
it.addEventListener('click', ()=> select(it));
it.addEventListener('mouseenter', ()=>{ idx=i; items.forEach(x=>x.classList.remove('is-active')); it.classList.add('is-active'); });
});
// keep hidden initial
menu.hidden=true;
})();
/* =======================
3. Tabs
======================= */
(() => {
const tablist=document.getElementById('tablist');
const tabs=[...tablist.querySelectorAll('[role="tab"]')];
const panels=tabs.map(t=> document.getElementById(t.getAttribute('aria-controls')));
function activate(newTab, moveFocus=true){
tabs.forEach(t=>{
const sel=t===newTab;
t.setAttribute('aria-selected', String(sel));
t.setAttribute('tabindex', sel ? '0':'-1');
});
panels.forEach(p=>{
const show = p.id===newTab.getAttribute('aria-controls');
if(show){ p.hidden=false; p.removeAttribute('hidden'); }
else { p.hidden=true; }
});
if(moveFocus) newTab.focus();
announce(`${newTab.textContent.trim()} tab selected`);
}
tablist.addEventListener('keydown', (e)=>{
const cur=tabs.indexOf(document.activeElement);
if(cur===-1) return;
let next=-1;
if(e.key==='ArrowRight') next=(cur+1)%tabs.length;
else if(e.key==='ArrowLeft') next=(cur-1+tabs.length)%tabs.length;
else if(e.key==='Home') next=0;
else if(e.key==='End') next=tabs.length-1;
else return;
e.preventDefault();
activate(tabs[next]);
});
tabs.forEach(t=> t.addEventListener('click', ()=> activate(t)));
tablist.addEventListener('focusout', ()=>{}); // keep
})();
/* =======================
4. Combobox (activedescendant)
======================= */
(() => {
const input=document.getElementById('comboInput');
const listbox=document.getElementById('comboListbox');
const status=document.getElementById('comboStatus');
const cities=[
["Lisbon","Portugal"],["Porto","Portugal"],["Madrid","Spain"],["Barcelona","Spain"],["Paris","France"],
["Lyon","France"],["Berlin","Germany"],["Munich","Germany"],["Rome","Italy"],["Milan","Italy"],
["Amsterdam","Netherlands"],["Brussels","Belgium"],["Vienna","Austria"],["Prague","Czechia"],
["Warsaw","Poland"],["Dublin","Ireland"],["Copenhagen","Denmark"],["Stockholm","Sweden"],
["Oslo","Norway"],["Helsinki","Finland"]
];
let filtered=[...cities];
let highlighted=-1;
let isOpen=false;
let originalValue='';
let lastSelected='';
function render(){
listbox.innerHTML='';
if(filtered.length===0){
listbox.innerHTML=`<li class="empty">No results for “${input.value}”</li>`;
return;
}
filtered.forEach(([city,country],i)=>{
const li=document.createElement('li');
li.id=`opt-${i}`;
li.setAttribute('role','option');
const isHi=i===highlighted;
li.className='option'+(isHi?' is-highlighted':'');
li.setAttribute('aria-selected', String(isHi));
li.innerHTML=`<span>${city}</span><span class="opt-sub">${country}</span>`;
li.addEventListener('mousedown', (e)=>{ e.preventDefault(); choose(i); });
listbox.appendChild(li);
});
}
function setExpanded(v){
isOpen=v;
input.setAttribute('aria-expanded', String(v));
listbox.classList.toggle('is-open', v);
listbox.hidden=!v;
if(v) render();
}
function filter(){
const q=input.value.trim().toLowerCase();
if(!q) filtered=[...cities];
else filtered=cities.filter(([c,co])=> c.toLowerCase().includes(q) || co.toLowerCase().includes(q));
highlighted = filtered.length ? 0 : -1;
syncActive();
render();
if(highlighted>=0) syncActive();
}
function syncActive(){
if(highlighted>=0 && filtered[highlighted]){
const id=`opt-${highlighted}`;
input.setAttribute('aria-activedescendant', id);
// scroll into view
requestAnimationFrame(()=>{
const el=document.getElementById(id);
if(el) el.scrollIntoView({block:'nearest'});
});
// update visual
[...listbox.children].forEach((ch,i)=>{
if(ch.classList.contains('option')){
ch.classList.toggle('is-highlighted', i===highlighted);
ch.setAttribute('aria-selected', String(i===highlighted));
}
});
} else {
input.removeAttribute('aria-activedescendant');
}
}
function choose(i){
if(i<0||i>=filtered.length) return;
const [city, country]=filtered[i];
input.value=city;
lastSelected=city;
status.innerHTML=`Selected <strong>${city}</strong> — ${country}`;
announce(`Selected ${city}, ${country}`);
setExpanded(false);
input.removeAttribute('aria-activedescendant');
highlighted=-1;
}
function revert(){
input.value=originalValue;
setExpanded(false);
input.removeAttribute('aria-activedescendant');
}
input.addEventListener('focus', ()=>{
originalValue=input.value;
filter();
setExpanded(true);
});
input.addEventListener('input', ()=>{
filter();
if(!isOpen) setExpanded(true);
else render();
status.textContent = filtered.length ? `${filtered.length} results` : 'No results';
});
input.addEventListener('keydown', (e)=>{
if(e.key==='ArrowDown'){
e.preventDefault();
if(!isOpen){ filter(); setExpanded(true); return; }
if(filtered.length===0) return;
highlighted=(highlighted+1)%filtered.length;
render(); syncActive();
} else if(e.key==='ArrowUp'){
e.preventDefault();
if(!isOpen){ filter(); setExpanded(true); return; }
highlighted=(highlighted-1+filtered.length)%filtered.length;
render(); syncActive();
} else if(e.key==='Enter'){
if(isOpen && highlighted>=0){ e.preventDefault(); choose(highlighted); }
} else if(e.key==='Escape'){
if(isOpen){ e.preventDefault(); revert(); }
} else if(e.key==='Tab'){
setExpanded(false);
} else if(e.key==='Home' || e.key==='End'){
// allow but not interfere
}
});
input.addEventListener('blur', (e)=>{
// delay to allow mousedown select
setTimeout(()=>{
if(!listbox.contains(document.activeElement) && document.activeElement!==input){
setExpanded(false);
input.removeAttribute('aria-activedescendant');
}
},150);
});
// initial hidden
listbox.hidden=true;
render();
})();
/* =======================
5. Accordion
======================= */
(() => {
const root=document.getElementById('accordionRoot');
const buttons=[...root.querySelectorAll('.acc-button')];
function toggle(btn){
const panel=document.getElementById(btn.getAttribute('aria-controls'));
const expanded=btn.getAttribute('aria-expanded')==='true';
const willOpen=!expanded;
btn.setAttribute('aria-expanded', String(willOpen));
if(willOpen){
// open: remove hidden, measure, animate
panel.hidden=false;
panel.style.height='0px';
panel.style.overflow='hidden';
const target=panel.firstElementChild.scrollHeight;
// force frame
requestAnimationFrame(()=>{
panel.style.height=target+'px';
});
const onEnd=()=>{
panel.style.height='';
panel.style.overflow='';
panel.removeEventListener('transitionend', onEnd);
};
panel.addEventListener('transitionend', onEnd);
// reduced motion: instantly
if(matchMedia('(prefers-reduced-motion: reduce)').matches){
panel.style.height='';
}
} else {
// close: animate to 0 then hidden
const start=panel.firstElementChild.scrollHeight;
panel.style.height=start+'px';
panel.style.overflow='hidden';
requestAnimationFrame(()=>{
requestAnimationFrame(()=> panel.style.height='0px');
});
const onEnd=()=>{
panel.hidden=true;
panel.style.height='';
panel.style.overflow='';
panel.removeEventListener('transitionend', onEnd);
};
panel.addEventListener('transitionend', onEnd);
if(matchMedia('(prefers-reduced-motion: reduce)').matches){
panel.hidden=true;
panel.style.height='';
}
// fallback if transitionend not fired
setTimeout(()=>{
if(panel.style.height==='0px'){ panel.hidden=true; panel.style.height=''; }
}, 400);
}
}
buttons.forEach(btn=>{
btn.addEventListener('click', ()=> toggle(btn));
// Enter/Space handled natively for button, but ensure
});
// set initial heights correctly
buttons.forEach(btn=>{
const panel=document.getElementById(btn.getAttribute('aria-controls'));
if(btn.getAttribute('aria-expanded')==='true'){
panel.hidden=false;
} else {
panel.hidden=true;
}
});
})();
/* =======================
Sidebar active state + validation
======================= */
(() => {
const links=[...document.querySelectorAll('.sidebar a')];
const sections=links.map(a=> document.querySelector(a.getAttribute('href')));
const obs=new IntersectionObserver((entries)=>{
entries.forEach(e=>{
if(e.isIntersecting){
links.forEach(l=> l.removeAttribute('aria-current'));
const match=links.find(l=> l.getAttribute('href')==='#'+e.target.id);
if(match) match.setAttribute('aria-current','true');
}
});
}, {rootMargin:'-40% 0px -50% 0px', threshold:0});
sections.forEach(s=> s && obs.observe(s));
validateAria();
})();
// Apply durations to CSS for reduced motion transparency
document.documentElement.style.setProperty('--dur-med', CONFIG.durations.medium+'ms');
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"482bb775-6623-4586-a6bb-1eac62ab6002","tokensIn":1312,"tokensOut":12984,"tokensTotal":14296,"cost":0.0566977,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:14:40.094Z"} -->