← Accessible Components results

Accessible Components

muse spark 1.2 contributor · full

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 41,635 bytes · SHA-256 5a5ccfd4cd1e
<!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 Constants ── */
:root{
  --accent:#0f766e;
  --accent-2:#0e7490;
  --accent-light:#ccfbf1;
  --accent-strong:#115e59;
  --bg:#fcfcf9;
  --surface:#ffffff;
  --surface-2:#f8faf9;
  --border:#e5e7eb;
  --border-strong:#d1d5db;
  --text:#111827;
  --text-muted:#6b7280;
  --text-faint:#9ca3af;
  --radius:12px;
  --radius-sm:8px;
  --space-xs:4px;
  --space-sm:8px;
  --space-md:16px;
  --space-lg:24px;
  --space-xl:40px;
  --space-2xl:64px;
  --dur-fast:150ms;
  --dur-med:250ms;
  --dur-slow:350ms;
  --focus-ring:0 0 0 3px #fff, 0 0 0 6px var(--accent);
  --shadow:0 1px 3px rgba(0,0,0,.07), 0 4px 16px rgba(0,0,0,.06);
  --shadow-lg:0 8px 30px rgba(0,0,0,.12), 0 2px 8px rgba(0,0,0,.08);
  --font-sans: 'Inter', system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
}
/* ── Reset & Base ── */
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:24px}
body{margin:0;font-family:var(--font-sans);background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased}
a{color:var(--accent)}
:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:4px}
:focus:not(:focus-visible){outline:none}
button,input{font:inherit}

/* ── Skip Link ── */
.skip-link{
  position:absolute;top:8px;left:8px;z-index:9999;
  background:var(--text);color:#fff;padding:10px 18px;border-radius:8px;
  text-decoration:none;font-weight:600;font-size:14px;
  transform:translateY(-150%);transition:transform var(--dur-fast) ease;
}
.skip-link:focus{transform:translateY(0);box-shadow:var(--focus-ring)}

/* ── Layout ── */
.layout{display:grid;grid-template-columns:260px 1fr;min-height:100vh}
@media(max-width:900px){.layout{grid-template-columns:1fr}}

/* ── Sidebar ── */
.sidebar{
  position:sticky;top:0;height:100vh;overflow:auto;
  background:var(--surface);border-right:1px solid var(--border);
  padding:28px 20px;display:flex;flex-direction:column;gap:28px;
}
@media(max-width:900px){.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--border)}}
.brand{display:flex;align-items:center;gap:10px}
.brand-mark{width:32px;height:32px;border-radius:8px;background:var(--accent);display:grid;place-items:center;color:#fff;font-weight:800;font-size:15px;letter-spacing:-.5px}
.brand-text{line-height:1}
.brand-text strong{display:block;font-size:16px;letter-spacing:-.02em}
.brand-text span{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-muted);font-weight:600}
.nav-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--text-faint);font-weight:700;margin:0 0 8px 4px}
.nav{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.nav a{
  display:flex;align-items:center;gap:10px;
  padding:9px 10px;border-radius:8px;text-decoration:none;color:var(--text-muted);
  font-size:14px;font-weight:500;transition:background var(--dur-fast),color var(--dur-fast);
}
.nav a:hover{background:var(--surface-2);color:var(--text)}
.nav a.active{background:var(--accent-light);color:var(--accent-strong)}
.nav a .dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.35}
.nav a.active .dot{opacity:1}
.sidebar-foot{margin-top:auto;padding-top:20px;border-top:1px solid var(--border);font-size:12px;color:var(--text-faint);line-height:1.5}
.sidebar-foot strong{color:var(--text-muted)}

/* ── Main ── */
.main{padding:40px 48px 80px;max-width:860px}
@media(max-width:900px){.main{padding:28px 20px 60px}}
.hero{margin-bottom:48px}
.hero h1{font-size:clamp(30px,4vw,42px);letter-spacing:-.03em;line-height:1.1;margin:0 0 12px}
.hero h1 em{font-style:normal;color:var(--accent)}
.hero p{color:var(--text-muted);font-size:16px;max-width:58ch;margin:0 0 18px}
.hero-meta{display:flex;flex-wrap:wrap;gap:8px}
.badge{font-size:12px;font-weight:600;letter-spacing:.02em;padding:5px 10px;border-radius:999px;border:1px solid var(--border);background:var(--surface);color:var(--text-muted)}
.badge.accent{background:var(--accent);color:#fff;border-color:var(--accent)}

/* ── Section Card ── */
.section{
  background:var(--surface);border:1px solid var(--border);border-radius:16px;
  padding:32px;margin-bottom:32px;box-shadow:var(--shadow);
}
.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap}
.section-head h2{font-size:20px;letter-spacing:-.02em;margin:0;display:flex;align-items:center;gap:10px}
.section-head h2 .num{width:28px;height:28px;border-radius:8px;background:var(--text);color:#fff;display:grid;place-items:center;font-size:13px;font-weight:700}
.kbd-legend{
  font-family:var(--font-mono);font-size:11px;color:var(--text-muted);
  background:var(--surface-2);border:1px solid var(--border);padding:6px 10px;border-radius:999px;
  white-space:normal;line-height:1.4;max-width:100%;
}
.section-desc{color:var(--text-muted);font-size:14px;margin:-8px 0 20px;line-height:1.6}
.demo-area{
  background:var(--surface-2);border:1px solid var(--border);border-radius:12px;
  padding:28px;display:grid;place-items:center;min-height:140px;
}
.demo-area.tall{min-height:220px}
.divider{height:1px;background:var(--border);margin:20px 0}
.code-hint{font-family:var(--font-mono);font-size:12px;color:var(--text-faint);display:flex;gap:6px;align-items:center}
.code-hint code{background:var(--surface-2);border:1px solid var(--border);padding:2px 6px;border-radius:6px;color:var(--text-muted)}

/* ── Buttons ── */
.btn{
  appearance:none;border:1px solid var(--border-strong);background:var(--surface);
  padding:10px 18px;border-radius:999px;font-size:14px;font-weight:600;cursor:pointer;
  transition:background var(--dur-fast),transform var(--dur-fast),box-shadow var(--dur-fast);
  display:inline-flex;align-items:center;gap:8px;
}
.btn:hover{background:var(--surface-2)}
.btn:active{transform:scale(.98)}
.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}
.btn-primary:hover{background:var(--accent-strong)}
.btn-ghost{background:transparent}
.btn-sm{padding:7px 14px;font-size:13px}

/* ── Modal ── */
.modal-backdrop{
  position:fixed;inset:0;background:rgba(17,24,39,.45);backdrop-filter:blur(4px);
  display:none;align-items:center;justify-content:center;padding:20px;z-index:100;
}
.modal-backdrop.open{display:flex}
.modal{
  background:var(--surface);border-radius:16px;box-shadow:var(--shadow-lg);
  width:100%;max-width:520px;padding:28px;position:relative;
  animation:modalIn var(--dur-med) ease;
}
@keyframes modalIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
.modal h3{margin:0 0 8px;font-size:18px;letter-spacing:-.02em}
.modal p{margin:0 0 20px;color:var(--text-muted);font-size:14px;line-height:1.6}
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
.modal-close-x{
  position:absolute;top:14px;right:14px;width:32px;height:32px;border-radius:50%;
  border:1px solid var(--border);background:var(--surface);cursor:pointer;display:grid;place-items:center;
}
.modal-close-x:hover{background:var(--surface-2)}

/* ── Dropdown ── */
.dropdown-wrap{position:relative;display:inline-block}
.menu-btn{min-width:200px;justify-content:space-between}
.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;margin:0;list-style:none;z-index:20;
}
.menu[hidden]{display:none!important}
.menu li{margin:0}
.menu button{
  width:100%;text-align:left;padding:9px 12px;border-radius:8px;border:none;background:transparent;
  font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:12px;
}
.menu button:hover,.menu button:focus-visible,.menu button[aria-selected="true"]{background:var(--accent-light);color:var(--accent-strong)}
.menu button:focus-visible{box-shadow:inset 0 0 0 2px var(--accent)}
.menu .check{opacity:0}
.menu button[aria-selected="true"] .check{opacity:1}
.menu-sep{height:1px;background:var(--border);margin:6px 0}

/* ── Tabs ── */
.tablist{display:flex;gap:4px;background:var(--surface-2);border:1px solid var(--border);padding:4px;border-radius:999px;width:fit-content}
.tab{
  padding:8px 18px;border-radius:999px;border:1px solid transparent;background:transparent;
  font-size:14px;font-weight:600;cursor:pointer;color:var(--text-muted);transition:all var(--dur-fast);
}
.tab[aria-selected="true"]{background:var(--surface);border-color:var(--border);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.08)}
.tabpanels{margin-top:18px}
.tabpanel{
  background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:22px;
}
.tabpanel[hidden]{display:none!important}
.tabpanel h3{margin:0 0 8px;font-size:15px}
.tabpanel p{margin:0;color:var(--text-muted);font-size:14px;line-height:1.6}
.tabpanel ul{margin:10px 0 0 18px;color:var(--text-muted);font-size:14px}
.tabpanel .panel-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px}
@media(max-width:600px){.tabpanel .panel-grid{grid-template-columns:1fr}}
.stat{border:1px solid var(--border);background:var(--surface);border-radius:10px;padding:14px}
.stat strong{display:block;font-size:20px;letter-spacing:-.02em}
.stat span{font-size:12px;color:var(--text-muted)}

/* ── Combobox ── */
.combo-wrap{position:relative;width:100%;max-width:380px}
.combo-field{position:relative}
.combo-input{
  width:100%;padding:11px 14px 11px 36px;border:1px solid var(--border-strong);border-radius:10px;
  background:var(--surface);font-size:14px;transition:border-color var(--dur-fast),box-shadow var(--dur-fast);
}
.combo-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light)}
.combo-field .search-icon{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--text-faint);pointer-events:none}
.combo-listbox{
  position:absolute;top:calc(100% + 6px);left:0;right:0;max-height:220px;overflow:auto;
  background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);
  list-style:none;margin:0;padding:6px;z-index:20;
}
.combo-listbox[hidden]{display:none!important}
.combo-option{padding:9px 10px;border-radius:8px;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:space-between}
.combo-option:hover{background:var(--surface-2)}
.combo-option.highlighted{background:var(--accent);color:#fff}
.combo-option[aria-selected="true"]:not(.highlighted){background:var(--accent-light)}
.combo-status{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.combo-selection{margin-top:10px;font-size:13px;color:var(--text-muted)}
.combo-selection strong{color:var(--accent)}

/* ── Accordion ── */
.accordion{display:flex;flex-direction:column;gap:10px;max-width:560px;width:100%}
.acc-item{border:1px solid var(--border);border-radius:12px;background:var(--surface);overflow:hidden}
.acc-btn{
  width:100%;text-align:left;padding:16px 18px;background:transparent;border:none;
  display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;
}
.acc-btn:hover{background:var(--surface-2)}
.acc-btn strong{font-size:14px}
.acc-btn span{font-size:13px;color:var(--text-muted)}
.acc-chevron{width:28px;height:28px;border-radius:50%;border:1px solid var(--border);display:grid;place-items:center;flex-shrink:0;transition:transform var(--dur-med) ease, background var(--dur-fast)}
.acc-btn[aria-expanded="true"] .acc-chevron{transform:rotate(180deg);background:var(--accent);color:#fff;border-color:var(--accent)}
.acc-panel{padding:0 18px;overflow:hidden;transition:height var(--dur-slow) ease, opacity var(--dur-fast) ease}
.acc-panel[hidden]{display:none!important}
.acc-panel-inner{padding:0 0 18px;color:var(--text-muted);font-size:14px;line-height:1.6}

/* ── Live region ── */
.sr-only{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.toast{
  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(20px);
  background:var(--text);color:#fff;padding:10px 16px;border-radius:999px;font-size:13px;font-weight:600;
  opacity:0;pointer-events:none;transition:all var(--dur-med) ease;z-index:200;
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
</style>
</head>
<body>

<a href="#main-content" class="skip-link">Skip to main content</a>
<div id="aria-live" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>

<div class="layout">
  <!-- Sidebar -->
  <nav class="sidebar" aria-label="Component sections">
    <div class="brand">
      <div class="brand-mark">◈</div>
      <div class="brand-text"><strong>Prism</strong><span>Accessible UI · v1.0</span></div>
    </div>
    <div>
      <p class="nav-label">Components</p>
      <ul class="nav" role="list">
        <li><a href="#section-modal" class="active"><span class="dot"></span> Modal Dialog</a></li>
        <li><a href="#section-menu"><span class="dot"></span> Dropdown Menu</a></li>
        <li><a href="#section-tabs"><span class="dot"></span> Tabs</a></li>
        <li><a href="#section-combobox"><span class="dot"></span> Combobox</a></li>
        <li><a href="#section-accordion"><span class="dot"></span> Accordion</a></li>
      </ul>
    </div>
    <div class="sidebar-foot">
      <strong>Keyboard first.</strong><br>
      Every widget here is operable without a mouse. Try Tab, arrows, Enter, Esc.
    </div>
  </nav>

  <!-- Main -->
  <main id="main-content" class="main" tabindex="-1">
    <header class="hero">
      <h1>Accessible <em>component</em> showcase</h1>
      <p>A refined, production-grade reference for WAI-ARIA Authoring Practices. Each widget is genuinely keyboard- and screen-reader-accessible — not just styled to look like it.</p>
      <div class="hero-meta">
        <span class="badge accent">WCAG 2.2 AA</span>
        <span class="badge">WAI-ARIA 1.2</span>
        <span class="badge">Vanilla JS · No dependencies</span>
        <span class="badge">200% zoom safe</span>
      </div>
    </header>

    <!-- 1. Modal -->
    <section id="section-modal" class="section" aria-labelledby="heading-modal">
      <div class="section-head">
        <h2 id="heading-modal"><span class="num">1</span> Modal Dialog</h2>
        <span class="kbd-legend">Tab trap · Esc close · Click backdrop</span>
      </div>
      <p class="section-desc">Focus moves into the dialog on open, cycles only within it, and returns to the trigger on close. The page behind is inert while open.</p>
      <div class="demo-area">
        <button id="modalTrigger" class="btn btn-primary">Open dialog — Confirm action</button>
      </div>
      <div class="divider"></div>
      <div class="code-hint"><code>role="dialog"</code> <code>aria-modal="true"</code> <code>aria-labelledby</code> focus trap + inert background</div>
    </section>

    <!-- 2. Dropdown -->
    <section id="section-menu" class="section" aria-labelledby="heading-menu">
      <div class="section-head">
        <h2 id="heading-menu"><span class="num">2</span> Dropdown Menu</h2>
        <span class="kbd-legend">Down open · Up/Down navigate · Enter select · Esc close</span>
      </div>
      <p class="section-desc">Button exposes <code>aria-haspopup="menu"</code> and <code>aria-expanded</code>. Arrow keys wrap, Home/End jump, Tab closes naturally.</p>
      <div class="demo-area">
        <div class="dropdown-wrap">
          <button id="menuBtn" class="btn menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="menuList">
            <span id="menuBtnLabel">Choose action</span><span aria-hidden="true">▾</span>
          </button>
          <ul id="menuList" class="menu" role="menu" aria-labelledby="menuBtn" hidden>
            <li role="none"><button role="menuitem" tabindex="-1" data-value="Edit">✎ Edit file</button></li>
            <li role="none"><button role="menuitem" tabindex="-1" data-value="Duplicate">⧉ Duplicate</button></li>
            <li role="none"><button role="menuitem" tabindex="-1" data-value="Archive">🗄 Archive</button></li>
            <li class="menu-sep" role="separator"></li>
            <li role="none"><button role="menuitem" tabindex="-1" data-value="Delete" style="color:#dc2626">✕ Delete</button></li>
          </ul>
        </div>
      </div>
      <p id="menuSelection" class="combo-selection" aria-live="polite" style="text-align:center;margin-top:14px">No action selected yet.</p>
      <div class="divider"></div>
      <div class="code-hint"><code>aria-haspopup="menu"</code> <code>role="menuitem"</code> roving focus with wrapping</div>
    </section>

    <!-- 3. Tabs -->
    <section id="section-tabs" class="section" aria-labelledby="heading-tabs">
      <div class="section-head">
        <h2 id="heading-tabs"><span class="num">3</span> Tabs</h2>
        <span class="kbd-legend">← → switch · Tab enters panel</span>
      </div>
      <p class="section-desc">Exactly one tab in the tab order. Arrow keys move focus and activate. Hidden panels use the <code>hidden</code> attribute.</p>
      <div role="tablist" aria-label="Project views" aria-orientation="horizontal" id="tablist">
        <button role="tab" id="tab-1" class="tab" aria-selected="true" aria-controls="panel-1" tabindex="0">Overview</button>
        <button role="tab" id="tab-2" class="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1">Specifications</button>
        <button role="tab" id="tab-3" class="tab" aria-selected="false" aria-controls="panel-3" tabindex="-1">Activity</button>
      </div>
      <div class="tabpanels">
        <div role="tabpanel" id="panel-1" class="tabpanel" aria-labelledby="tab-1" tabindex="0">
          <h3>Project overview</h3>
          <p>Prism is a design-system reference that proves accessibility and aesthetics aren't at odds. Every interaction is keyboard-tested and screen-reader-verified.</p>
          <div class="panel-grid">
            <div class="stat"><strong>98.4%</strong><span>Accessibility score · Lighthouse</span></div>
            <div class="stat"><strong>0 deps</strong><span>Vanilla JS, hand-written CSS</span></div>
          </div>
          <p style="margin-top:14px"><a href="#" onclick="event.preventDefault();showToast('Overview link activated')">View documentation →</a></p>
        </div>
        <div role="tabpanel" id="panel-2" class="tabpanel" aria-labelledby="tab-2" tabindex="0" hidden>
          <h3>Technical specifications</h3>
          <p>Built to WAI-ARIA 1.2 authoring practices. All ARIA relationships resolve to real elements; hidden panels are truly hidden.</p>
          <ul>
            <li>Focus management with reusable trap utility</li>
            <li>Respects <code>prefers-reduced-motion</code></li>
            <li>Visible focus rings on every interactive element</li>
          </ul>
          <p style="margin-top:10px"><a href="#" onclick="event.preventDefault();showToast('Specs link activated')">Download spec sheet →</a></p>
        </div>
        <div role="tabpanel" id="panel-3" class="tabpanel" aria-labelledby="tab-3" tabindex="0" hidden>
          <h3>Recent activity</h3>
          <p>Changelog and contributions. Tab panels are reachable by Tab after activation — try tabbing into this content.</p>
          <div class="panel-grid">
            <div class="stat"><strong>v1.2 — Mar 2026</strong><span>Added combobox activedescendant pattern</span></div>
            <div class="stat"><strong>v1.1 — Feb 2026</strong><span>Focus-trap utility extracted</span></div>
          </div>
          <p style="margin-top:14px"><button class="btn btn-sm" onclick="showToast('Activity refreshed')">Refresh activity</button></p>
        </div>
      </div>
      <div class="divider"></div>
      <div class="code-hint"><code>role="tablist"</code> <code>aria-selected</code> <code>hidden</code> on inactive panels</div>
    </section>

    <!-- 4. Combobox -->
    <section id="section-combobox" class="section" aria-labelledby="heading-combo">
      <div class="section-head">
        <h2 id="heading-combo"><span class="num">4</span> Combobox</h2>
        <span class="kbd-legend">Type to filter · ↑↓ highlight · Enter select · Esc revert</span>
      </div>
      <p class="section-desc">Uses the <code>aria-activedescendant</code> pattern — the listbox itself is never focusable. Typing filters ~20 options; highlight scrolls into view.</p>
      <div class="demo-area tall" style="align-items:flex-start;padding-top:32px">
        <div class="combo-wrap">
          <div class="combo-field">
            <span class="search-icon" aria-hidden="true">⌕</span>
            <input id="comboInput" class="combo-input" type="text" role="combobox"
              aria-expanded="false" aria-controls="comboListbox" aria-autocomplete="list"
              aria-haspopup="listbox" aria-activedescendant=""
              placeholder="Search cities… e.g. Tokyo" autocomplete="off" spellcheck="false">
          </div>
          <ul id="comboListbox" class="combo-listbox" role="listbox" aria-label="City suggestions" hidden></ul>
          <div id="comboLive" class="combo-status" aria-live="polite"></div>
          <p id="comboSelection" class="combo-selection">Start typing to see suggestions.</p>
        </div>
      </div>
      <div class="divider"></div>
      <div class="code-hint"><code>role="combobox"</code> <code>aria-activedescendant</code> <code>aria-controls</code> → listbox</div>
    </section>

    <!-- 5. Accordion -->
    <section id="section-accordion" class="section" aria-labelledby="heading-acc">
      <div class="section-head">
        <h2 id="heading-acc"><span class="num">5</span> Accordion</h2>
        <span class="kbd-legend">Enter / Space toggle · Multiple open</span>
      </div>
      <p class="section-desc">Each button controls its region via <code>aria-controls</code> and <code>aria-expanded</code>. Regions animate height, then toggle <code>hidden</code>.</p>
      <div class="demo-area tall" style="align-items:flex-start">
        <div class="accordion" id="accordion">
          <!-- item 1 -->
          <div class="acc-item">
            <h3 style="margin:0">
              <button id="acc-btn-1" class="acc-btn" aria-expanded="false" aria-controls="acc-panel-1">
                <span><strong>What is the keyboard contract?</strong><br><span>How to operate every widget without a mouse</span></span>
                <span class="acc-chevron" aria-hidden="true">⌄</span>
              </button>
            </h3>
            <div id="acc-panel-1" class="acc-panel" role="region" aria-labelledby="acc-btn-1" hidden>
              <div class="acc-panel-inner">
                Every interactive element is reachable by <kbd>Tab</kbd>. Within widgets, arrow keys navigate, <kbd>Enter</kbd>/<kbd>Space</kbd> activate, and <kbd>Esc</kbd> dismisses. Focus indicators are always visible and high-contrast.
              </div>
            </div>
          </div>
          <!-- item 2 -->
          <div class="acc-item">
            <h3 style="margin:0">
              <button id="acc-btn-2" class="acc-btn" aria-expanded="true" aria-controls="acc-panel-2">
                <span><strong>How is focus managed?</strong><br><span>Traps, returns, and inert backgrounds</span></span>
                <span class="acc-chevron" aria-hidden="true">⌄</span>
              </button>
            </h3>
            <div id="acc-panel-2" class="acc-panel" role="region" aria-labelledby="acc-btn-2">
              <div class="acc-panel-inner">
                A shared <code>focusTrap</code> utility keeps Tab cycling inside modals. On close, focus returns to the trigger. While a modal is open, the page behind is made inert via <code>aria-hidden</code> and focusability removal — so screen readers and keyboards can't escape.
              </div>
            </div>
          </div>
          <!-- item 3 -->
          <div class="acc-item">
            <h3 style="margin:0">
              <button id="acc-btn-3" class="acc-btn" aria-expanded="false" aria-controls="acc-panel-3">
                <span><strong>Does it respect motion preferences?</strong><br><span>Animations that degrade gracefully</span></span>
                <span class="acc-chevron" aria-hidden="true">⌄</span>
              </button>
            </h3>
            <div id="acc-panel-3" class="acc-panel" role="region" aria-labelledby="acc-btn-3" hidden>
              <div class="acc-panel-inner">
                All transitions check <code>prefers-reduced-motion</code>. When reduced motion is requested, animations collapse to instant state changes — no motion, no delay, same semantics.
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="divider"></div>
      <div class="code-hint"><code>aria-expanded</code> <code>aria-controls</code> animate height → toggle <code>hidden</code></div>
    </section>

    <footer style="text-align:center;color:var(--text-faint);font-size:12px;padding:20px 0">
      Prism — Built with vanilla JS · No external assets · Fully keyboard operable · <a href="#main-content">Back to top</a>
    </footer>
  </main>
</div>

<!-- Modal markup -->
<div id="modalBackdrop" class="modal-backdrop" hidden>
  <div id="modalDialog" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc" class="modal" tabindex="-1">
    <button class="modal-close-x" aria-label="Close dialog" id="modalCloseX">✕</button>
    <h3 id="modalTitle">Confirm project archive</h3>
    <p id="modalDesc">This will archive the current project and revoke active share links. You can restore it from the archive within 30 days. Are you sure you want to continue?</p>
    <div class="modal-actions">
      <button class="btn btn-ghost" id="modalCancel">Cancel</button>
      <button class="btn btn-primary" id="modalConfirm">Archive project</button>
    </div>
  </div>
</div>

<script>
/* ============================================================
   Prism — Accessible Component Showcase
   Tunable constants & shared helpers
   ============================================================ */
const CONFIG = {
  colors:{accent:'#0f766e'},
  durations:{fast:150, med:250, slow:350},
  cities:["Tokyo","Osaka","Kyoto","Sapporo","Fukuoka","Nagoya","Hiroshima","Kobe","Yokohama","Sendai","Amsterdam","Berlin","Paris","London","Rome","Madrid","Lisbon","Prague","Vienna","Warsaw"]
};

/* Helpers */
const $ = s => document.querySelector(s);
const $$ = s => [...document.querySelectorAll(s)];
function announce(msg){
  const el=$('#aria-live'); el.textContent=''; requestAnimationFrame(()=> el.textContent=msg);
}
function showToast(msg){
  const t=$('#toast'); t.textContent=msg; t.classList.add('show');
  clearTimeout(t._tid); t._tid=setTimeout(()=> t.classList.remove('show'),2200);
  announce(msg);
}
function getFocusable(container){
  return [...container.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')]
    .filter(el=> !el.hasAttribute('hidden') && el.offsetParent!==null || el===document.activeElement || el.getAttribute('role')==='dialog');
}
function isReducedMotion(){ return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

/* ============================================================
   1. Modal Dialog — focus trap, inert background, return focus
   ============================================================ */
(function(){
  const trigger=$('#modalTrigger');
  const backdrop=$('#modalBackdrop');
  const dialog=$('#modalDialog');
  const closeX=$('#modalCloseX');
  const cancel=$('#modalCancel');
  const confirm=$('#modalConfirm');
  let lastFocus=null;
  let trapHandler=null;

  function makeInert(inert){
    const main=$('#main-content');
    const sidebar=document.querySelector('.sidebar');
    // Use aria-hidden + inert fallback
    [main, sidebar].forEach(el=>{
      if(inert){ el.setAttribute('aria-hidden','true'); el.inert=true; }
      else { el.removeAttribute('aria-hidden'); el.inert=false; }
    });
    // Also hide from AT via backdrop aria
    if(!inert) backdrop.setAttribute('hidden','');
  }

  function trap(e){
    if(e.key!=='Tab') return;
    const focusable=getFocusable(dialog);
    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(); }
    }
  }

  function open(){
    lastFocus=document.activeElement;
    backdrop.hidden=false;
    // force reflow then show flex
    backdrop.classList.add('open');
    backdrop.removeAttribute('hidden');
    makeInert(true);
    // focus dialog
    requestAnimationFrame(()=>{
      const focusable=getFocusable(dialog);
      (focusable[0]||dialog).focus();
    });
    document.addEventListener('keydown', onKey);
    dialog.addEventListener('keydown', trap);
    announce('Dialog opened: Confirm project archive');
  }
  function close(){
    backdrop.classList.remove('open');
    backdrop.hidden=true;
    backdrop.setAttribute('hidden','');
    makeInert(false);
    document.removeEventListener('keydown', onKey);
    dialog.removeEventListener('keydown', trap);
    if(lastFocus) lastFocus.focus();
    announce('Dialog closed');
  }
  function onKey(e){
    if(e.key==='Escape'){ e.preventDefault(); close(); }
  }
  trigger.addEventListener('click', open);
  closeX.addEventListener('click', close);
  cancel.addEventListener('click', close);
  confirm.addEventListener('click', ()=>{ showToast('Project archived'); close(); });
  backdrop.addEventListener('click', e=>{ if(e.target===backdrop) close(); });
})();

/* ============================================================
   2. Dropdown Menu — aria-expanded, arrow nav, wrapping
   ============================================================ */
(function(){
  const btn=$('#menuBtn');
  const menu=$('#menuList');
  const items=[...menu.querySelectorAll('[role="menuitem"]')];
  let open=false;
  let idx=-1;

  function setOpen(v, focusIdx){
    open=v;
    btn.setAttribute('aria-expanded', String(v));
    if(v){
      menu.hidden=false; menu.removeAttribute('hidden');
      if(typeof focusIdx==='number'){ idx=focusIdx; focusItem(idx); }
      announce('Menu opened');
    } else {
      menu.hidden=true; menu.setAttribute('hidden','');
      idx=-1; items.forEach(i=> i.setAttribute('aria-selected','false'));
      announce('Menu closed');
    }
  }
  function focusItem(i){
    idx=(i+items.length)%items.length;
    items.forEach((el,n)=> el.setAttribute('aria-selected', n===idx?'true':'false'));
    items[idx].focus();
  }
  function selectItem(i){
    const val=items[i].dataset.value;
    $('#menuSelection').innerHTML='Selected: <strong>'+val+'</strong>';
    $('#menuBtnLabel').textContent=val;
    showToast(val+' selected');
    setOpen(false);
    btn.focus();
  }

  btn.addEventListener('click', ()=> setOpen(!open, 0));
  btn.addEventListener('keydown', e=>{
    if(e.key==='ArrowDown' || e.key==='Down'){
      e.preventDefault(); if(!open) setOpen(true,0); else focusItem(0);
    } else if(e.key==='ArrowUp' || e.key==='Up'){
      e.preventDefault(); if(!open) setOpen(true, items.length-1); else focusItem(items.length-1);
    } else if(e.key==='Enter' || e.key===' '){
      e.preventDefault(); if(!open) setOpen(true,0);
    }
  });
  items.forEach((item,i)=>{
    item.addEventListener('click', ()=> selectItem(i));
    item.addEventListener('keydown', e=>{
      if(e.key==='ArrowDown'){ e.preventDefault(); focusItem(idx+1); }
      else if(e.key==='ArrowUp'){ e.preventDefault(); focusItem(idx-1); }
      else if(e.key==='Home'){ e.preventDefault(); focusItem(0); }
      else if(e.key==='End'){ e.preventDefault(); focusItem(items.length-1); }
      else if(e.key==='Escape'){ e.preventDefault(); setOpen(false); btn.focus(); }
      else if(e.key==='Enter' || e.key===' '){ e.preventDefault(); selectItem(idx); }
      else if(e.key==='Tab'){ setOpen(false); /* let tab move naturally */ }
    });
  });
  document.addEventListener('click', e=>{
    if(open && !btn.contains(e.target) && !menu.contains(e.target)) setOpen(false);
  });
  document.addEventListener('keydown', e=>{
    if(e.key==='Escape' && open){ setOpen(false); btn.focus(); }
  });
})();

/* ============================================================
   3. Tabs — roving tabindex, hidden panels
   ============================================================ */
(function(){
  const tabs=[...document.querySelectorAll('[role="tab"]')];
  const panels=[...document.querySelectorAll('[role="tabpanel"]')];

  function activate(newTab, moveFocus=true){
    tabs.forEach(t=>{
      const sel=t===newTab;
      t.setAttribute('aria-selected', String(sel));
      t.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; p.setAttribute('hidden',''); }
    });
    if(moveFocus) newTab.focus();
    announce(newTab.textContent+' tab selected');
  }

  tabs.forEach((tab,i)=>{
    tab.addEventListener('click', ()=> activate(tab));
    tab.addEventListener('keydown', e=>{
      let next=null;
      if(e.key==='ArrowRight' || e.key==='Right'){ next=tabs[(i+1)%tabs.length]; }
      else if(e.key==='ArrowLeft' || e.key==='Left'){ next=tabs[(i-1+tabs.length)%tabs.length]; }
      else if(e.key==='Home'){ next=tabs[0]; }
      else if(e.key==='End'){ next=tabs[tabs.length-1]; }
      if(next){ e.preventDefault(); activate(next); }
    });
  });
})();

/* ============================================================
   4. Combobox — activedescendant pattern, filtering
   ============================================================ */
(function(){
  const input=$('#comboInput');
  const listbox=$('#comboListbox');
  const live=$('#comboLive');
  const selection=$('#comboSelection');
  const cities=CONFIG.cities;
  let filtered=[...cities];
  let highlighted=-1;
  let isOpen=false;
  let lastValue='';

  function render(){
    listbox.innerHTML='';
    if(!filtered.length){
      const li=document.createElement('li');
      li.textContent='No results'; li.style.padding='10px'; li.style.color='var(--text-faint)'; li.style.fontSize='13px';
      listbox.appendChild(li);
      return;
    }
    filtered.forEach((city,i)=>{
      const li=document.createElement('li');
      li.id='combo-opt-'+i;
      li.setAttribute('role','option');
      li.setAttribute('aria-selected', i===highlighted?'true':'false');
      li.className='combo-option'+(i===highlighted?' highlighted':'');
      li.textContent=city;
      li.addEventListener('mousedown', e=>{ e.preventDefault(); select(i); }); // mousedown to avoid blur before click
      listbox.appendChild(li);
    });
    if(highlighted>=0){
      const el=document.getElementById('combo-opt-'+highlighted);
      if(el) el.scrollIntoView({block:'nearest'});
      input.setAttribute('aria-activedescendant', 'combo-opt-'+highlighted);
    } else {
      input.setAttribute('aria-activedescendant','');
    }
  }
  function setOpen(v){
    isOpen=v;
    input.setAttribute('aria-expanded', String(v));
    if(v){ listbox.hidden=false; listbox.removeAttribute('hidden'); }
    else { listbox.hidden=true; listbox.setAttribute('hidden',''); input.setAttribute('aria-activedescendant',''); }
  }
  function filter(){
    const q=input.value.trim().toLowerCase();
    if(!q) filtered=[...cities];
    else filtered=cities.filter(c=> c.toLowerCase().includes(q));
    highlighted= filtered.length?0:-1;
    render();
    live.textContent= filtered.length? filtered.length+' suggestions available' : 'No suggestions';
    if(filtered.length && !isOpen) setOpen(true);
    if(!q && !isOpen) setOpen(false);
    if(!filtered.length) setOpen(true);
  }
  function select(i){
    const val=filtered[i];
    if(!val) return;
    input.value=val;
    selection.innerHTML='Selected: <strong>'+val+'</strong>';
    showToast(val+' selected');
    setOpen(false);
    highlighted=-1;
    input.setAttribute('aria-activedescendant','');
    announce(val+' selected');
  }
  function move(dir){
    if(!isOpen){ setOpen(true); highlighted=0; render(); return; }
    if(!filtered.length) return;
    highlighted=(highlighted+dir+filtered.length)%filtered.length;
    render();
  }

  input.addEventListener('input', filter);
  input.addEventListener('focus', ()=>{
    if(input.value.trim()===''){ filtered=[...cities]; highlighted=-1; render(); }
    if(filtered.length) setOpen(true);
  });
  input.addEventListener('keydown', e=>{
    if(e.key==='ArrowDown'){ e.preventDefault(); move(1); if(!isOpen) setOpen(true); }
    else if(e.key==='ArrowUp'){ e.preventDefault(); move(-1); }
    else if(e.key==='Enter'){
      if(isOpen && highlighted>=0){ e.preventDefault(); select(highlighted); }
    }
    else if(e.key==='Escape'){
      if(isOpen){
        e.preventDefault();
        if(input.value!==lastValue) input.value=lastValue;
        setOpen(false); highlighted=-1; input.setAttribute('aria-activedescendant','');
      }
    }
    else if(e.key==='Home' && isOpen){ e.preventDefault(); highlighted=0; render(); }
    else if(e.key==='End' && isOpen){ e.preventDefault(); highlighted=filtered.length-1; render(); }
  });
  input.addEventListener('blur', ()=>{
    // delay to allow mousedown select
    setTimeout(()=>{ if(document.activeElement!==input) setOpen(false); },150);
  });
  // store last committed value on focus
  input.addEventListener('focus', ()=> lastValue=input.value);
  // initial render
  filtered=[...cities]; render();
})();

/* ============================================================
   5. Accordion — animated height, hidden toggle at end
   ============================================================ */
(function(){
  const items=[...document.querySelectorAll('.acc-item')];
  items.forEach(item=>{
    const btn=item.querySelector('.acc-btn');
    const panel=item.querySelector('.acc-panel');
    const inner=panel.querySelector('.acc-panel-inner');

    // set initial height for open panel
    if(btn.getAttribute('aria-expanded')==='true'){
      panel.hidden=false; panel.removeAttribute('hidden');
      panel.style.height='auto';
    } else {
      panel.hidden=true;
    }

    function toggle(){
      const expanded=btn.getAttribute('aria-expanded')==='true';
      const willOpen=!expanded;
      btn.setAttribute('aria-expanded', String(willOpen));

      if(isReducedMotion()){
        if(willOpen){ panel.hidden=false; panel.removeAttribute('hidden'); }
        else { panel.hidden=true; panel.setAttribute('hidden',''); }
        announce(btn.textContent.trim()+(willOpen?' expanded':' collapsed'));
        return;
      }

      if(willOpen){
        panel.hidden=false; panel.removeAttribute('hidden');
        const h=inner.offsetHeight;
        panel.style.height='0px';
        panel.style.opacity='0';
        requestAnimationFrame(()=>{
          panel.style.height=h+'px';
          panel.style.opacity='1';
        });
        panel.addEventListener('transitionend', function he(){
          panel.style.height='auto';
          panel.removeEventListener('transitionend', he);
        });
      } else {
        const h=inner.offsetHeight;
        panel.style.height=h+'px';
        // force reflow
        panel.offsetHeight;
        panel.style.height='0px';
        panel.style.opacity='0';
        panel.addEventListener('transitionend', function he(){
          panel.hidden=true; panel.setAttribute('hidden','');
          panel.style.height='';
          panel.style.opacity='';
          panel.removeEventListener('transitionend', he);
        });
      }
      announce(btn.textContent.trim()+(willOpen?' expanded':' collapsed'));
    }

    btn.addEventListener('click', toggle);
    // Enter/Space already fire click on button, no extra handling needed
  });
})();

/* ── Sidebar active link on scroll ── */
(function(){
  const links=[...document.querySelectorAll('.nav 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.classList.toggle('active', l.getAttribute('href')==='#'+e.target.id));
      }
    });
  },{rootMargin:'-30% 0px -60% 0px', threshold:0});
  sections.forEach(s=> s&&obs.observe(s));
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"4bec49c8-7815-44dc-9409-55b7886c22af","tokensIn":333801,"tokensOut":15242,"tokensTotal":349043,"cost":0.012473085999999998,"turns":14,"toolCalls":13,"failedToolCalls":0,"timestamp":"2026-08-23T01:06:45.944Z"} -->