← Accessible Components results

Accessible Components

muse spark 1.2 · basic

Initial view of Accessible Components

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 &amp; 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"} -->