← Accessible Components results

Accessible Components

Qwen 3.8 · 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 49,382 bytes · SHA-256 af7674196b35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AriaKit — Accessible Component Showcase</title>
<style>
/* ═══════════════════════════════════════════════════════════
   Tunable constants (CSS side)
   Keep durations in sync with CONFIG.durations in the script.
═══════════════════════════════════════════════════════════ */
:root {
  /* palette — restrained two-accent system (indigo + teal) */
  --bg: #f6f6f2;
  --surface: #ffffff;
  --ink: #17191e;
  --ink-2: #3f4551;
  --muted: #68707e;
  --border: #e5e6e0;
  --border-2: #d3d5cd;
  --accent: #2f4fd0;
  --accent-ink: #2440ab;
  --accent-soft: #edf0fc;
  --accent-2: #0d7a63;
  --accent-2-soft: #e5f3ef;
  --danger: #b23a2e;
  --danger-soft: #fbecea;
  --code-bg: #15171c;
  --code-ink: #d8dbe3;
  --focus-ring: #2f4fd0;

  /* spacing scale */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 72px;

  /* type */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;

  /* motion */
  --dur-fast: 140ms;
  --dur-med: 220ms;
  --dur-slow: 340ms;

  /* geometry */
  --radius: 10px;
  --radius-sm: 7px;
  --sidebar-w: 248px;
  --shadow-pop: 0 16px 40px rgba(23, 25, 30, 0.16), 0 2px 8px rgba(23, 25, 30, 0.08);
}

/* ═══════════════ Base ═══════════════ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 16px/1.6 var(--font-ui);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent-soft); }

/* High-contrast focus indicator on every interactive element. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

code {
  font: 500 0.86em/1.4 var(--font-mono);
  background: var(--accent-soft);
  color: var(--accent-ink);
  padding: 1px 5px;
  border-radius: 4px;
}
.link {
  color: var(--accent-ink);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link:hover { color: var(--accent); }
pre code { background: none; color: inherit; padding: 0; }

kbd {
  display: inline-block;
  font: 500 11.5px/1 var(--font-mono);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 2px 6px;
  white-space: nowrap;
}

.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;
}

/* ═══════════════ Skip link ═══════════════ */
.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: -72px;
  z-index: 200;
  background: var(--ink);
  color: #fff;
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  padding: 10px 18px;
  border-radius: 8px;
  transition: top var(--dur-fast) ease;
}
.skip-link:focus {
  top: var(--sp-4);
  outline-offset: 4px;
}

/* ═══════════════ Layout ═══════════════ */
.page {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}
.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: auto;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}
main {
  width: 100%;
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--sp-7) clamp(24px, 5vw, 72px) var(--sp-8);
}
main:focus { outline: none; } /* skip-link target; section headings carry the context */

/* ═══════════════ Sidebar ═══════════════ */
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
  width: 20px; height: 20px;
  border-radius: 6px;
  background: var(--accent);
  position: relative;
  flex: none;
}
.brand-mark::after {
  content: "";
  position: absolute;
  right: 4px; bottom: 4px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent-2);
}
.brand-name { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; }
.brand-chip {
  margin-left: auto;
  font: 500 11px/1 var(--font-mono);
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 8px;
}
.side-nav { display: flex; flex-direction: column; gap: 2px; }
.side-nav a {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  color: var(--ink-2);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.side-nav a .idx {
  font: 500 11px/1 var(--font-mono);
  color: var(--muted);
  transition: color var(--dur-fast) ease;
}
.side-nav a:hover { background: var(--bg); color: var(--ink); }
.side-nav a.active { background: var(--accent-soft); color: var(--accent-ink); }
.side-nav a.active .idx { color: var(--accent-ink); }
.side-note {
  margin: auto 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

/* ═══════════════ Page head & sections ═══════════════ */
.page-head { margin-bottom: var(--sp-5); }
.eyebrow {
  margin: 0;
  font: 600 11px/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
h1 {
  margin: 10px 0 12px;
  font-size: 34px;
  line-height: 1.15;
  font-weight: 650;
  letter-spacing: -0.02em;
}
.lede { margin: 0; color: var(--muted); font-size: 15px; max-width: 62ch; }
.component { margin-top: var(--sp-8); }
.component-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4);
  border-bottom: 1px solid var(--border-2);
  padding-bottom: var(--sp-4);
}
.component h2 {
  margin: 6px 0 0;
  font-size: 24px;
  font-weight: 650;
  letter-spacing: -0.01em;
}
.component .lede { margin-top: var(--sp-4); }
.pattern-tag {
  font: 500 11px/1 var(--font-mono);
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 10px;
  white-space: nowrap;
}

/* demo card + caption */
.demo {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-5);
  margin-top: var(--sp-5);
  box-shadow: 0 1px 2px rgba(23, 25, 30, 0.04);
}
.caption {
  margin: var(--sp-3) 0 0;
  font-size: 13px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  align-items: center;
}
.status {
  margin: 14px 0 0;
  min-height: 1.5em;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--accent-2);
}

/* ═══════════════ Buttons ═══════════════ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: 500 14px/1 var(--font-ui);
  padding: 9px 16px;
  border-radius: 8px;
  border: 1px solid var(--border-2);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.btn:hover { background: var(--bg); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); }
.chev { flex: none; color: var(--muted); }
.btn .chev { transition: transform var(--dur-med) ease; }

/* ═══════════════ 02 · Dropdown menu ═══════════════ */
.menu-wrap { position: relative; display: inline-block; }
.menu {
  position: absolute;
  z-index: 60;
  top: calc(100% + 6px);
  left: 0;
  min-width: 236px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.menu[hidden] { display: none; }
.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 9px 10px;
  border: 0;
  background: none;
  border-radius: var(--radius-sm);
  font: 500 14px/1.2 var(--font-ui);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur-fast) ease;
}
.menu-item svg { flex: none; color: var(--muted); }
.menu-item:hover,
.menu-item:focus {
  background: var(--accent-soft);
  color: var(--accent-ink);
}
.menu-item:hover svg,
.menu-item:focus svg { color: var(--accent-ink); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger svg { color: var(--danger); }
.menu-item.danger:hover,
.menu-item.danger:focus { background: var(--danger-soft); color: var(--danger); }
.menu-sep { height: 1px; background: var(--border); margin: 4px 6px; }

/* ═══════════════ 03 · Tabs ═══════════════ */
.tablist {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--border-2);
}
.tab {
  border: 0;
  background: none;
  padding: 10px 14px;
  margin-bottom: -1px;
  font: 500 14px/1.2 var(--font-ui);
  color: var(--muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] {
  color: var(--accent-ink);
  font-weight: 600;
  border-bottom-color: var(--accent);
}
.tabpanel { padding-top: var(--sp-4); }
.tabpanel p { margin: 0 0 var(--sp-3); max-width: 70ch; font-size: 14.5px; color: var(--ink-2); }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.chip {
  font: 500 12px/1 var(--font-mono);
  padding: 5px 9px;
  border-radius: 999px;
  background: var(--accent-2-soft);
  color: var(--accent-2);
  border: 1px solid rgba(13, 122, 99, 0.25);
}
pre {
  background: var(--code-bg);
  color: var(--code-ink);
  padding: 14px 16px;
  border-radius: 8px;
  overflow: auto;
  font: 400 12.5px/1.65 var(--font-mono);
  margin: 0;
}
.attr-list { margin: 0; padding-left: 20px; font-size: 14.5px; color: var(--ink-2); }
.attr-list li { margin-bottom: 8px; }

/* ═══════════════ 04 · Combobox ═══════════════ */
.cb-wrap { position: relative; max-width: 400px; }
.cb-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  margin-bottom: 6px;
}
.cb-input {
  width: 100%;
  font: 400 15px/1.4 var(--font-ui);
  padding: 10px 12px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  transition: border-color var(--dur-fast) ease;
}
.cb-input::placeholder { color: var(--muted); }
.cb-input:focus { border-color: var(--accent); }
.cb-listbox {
  position: absolute;
  z-index: 60;
  top: calc(100% + 6px);
  left: 0; right: 0;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  max-height: 252px;
  overflow: auto;
}
.cb-listbox[hidden] { display: none; }
.cb-option {
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  cursor: pointer;
}
.cb-option.active { background: var(--accent-soft); color: var(--accent-ink); font-weight: 500; }
.cb-empty { padding: 8px 10px; color: var(--muted); font-size: 14px; font-style: italic; }

/* ═══════════════ 05 · Accordion ═══════════════ */
.acc {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.acc-item + .acc-item { border-top: 1px solid var(--border); }
.acc-h { margin: 0; font-size: 15px; }
.acc-btn {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-4);
  padding: 14px 18px;
  border: 0;
  background: none;
  font: 600 15px/1.3 var(--font-ui);
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  transition: background var(--dur-fast) ease;
}
.acc-btn:hover { background: var(--bg); }
.acc-btn .chev { transition: transform var(--dur-med) ease; }
.acc-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
.acc-region { overflow: hidden; }
.acc-inner { padding: 2px 18px 16px; }
.acc-inner p { margin: 0; font-size: 14.5px; color: var(--ink-2); max-width: 70ch; }

/* ═══════════════ 01 · Modal dialog ═══════════════ */
#modal-root {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}
#modal-root[hidden] { display: none; }
.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(23, 25, 30, 0.44);
}
.modal {
  position: relative;
  width: min(448px, 100%);
  background: var(--surface);
  border-radius: 14px;
  box-shadow: var(--shadow-pop);
  padding: var(--sp-6);
}
.modal h2 { margin: 0; font-size: 19px; font-weight: 650; letter-spacing: -0.01em; }
.modal .desc { margin: 8px 0 0; color: var(--muted); font-size: 14.5px; }
.modal .field {
  margin-top: 16px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  color: var(--ink-2);
  cursor: pointer;
}
.modal .field input { width: 16px; height: 16px; accent-color: var(--accent); }
.modal .actions { margin-top: 22px; display: flex; justify-content: flex-end; gap: 10px; }

/* ═══════════════ Footer ═══════════════ */
.page-foot {
  margin-top: var(--sp-8);
  border-top: 1px solid var(--border-2);
  padding-top: var(--sp-5);
  font-size: 13px;
  color: var(--muted);
}
.page-foot a { color: var(--accent-ink); }

/* ═══════════════ Motion ═══════════════ */
@media (prefers-reduced-motion: no-preference) {
  .modal { animation: pop-in var(--dur-med) cubic-bezier(0.2, 0.9, 0.3, 1.15); }
  .modal-backdrop { animation: fade-in var(--dur-med) ease; }
}
@keyframes pop-in {
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
}
@keyframes fade-in {
  from { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ═══════════════ Responsive ═══════════════ */
@media (max-width: 860px) {
  .page { grid-template-columns: 1fr; }
  .sidebar {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    gap: var(--sp-4);
  }
  .side-nav { flex-direction: row; flex-wrap: wrap; }
  .side-note { display: none; }
  .component-head { flex-direction: column; align-items: flex-start; }
  h1 { font-size: 28px; }
}
</style>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>

<div class="page">
  <aside class="sidebar">
    <div class="brand">
      <span class="brand-mark" aria-hidden="true"></span>
      <span class="brand-name">AriaKit</span>
      <span class="brand-chip">v2.4</span>
    </div>
    <nav class="side-nav" aria-label="Components">
      <a href="#modal"><span class="idx" aria-hidden="true">01</span> Modal Dialog</a>
      <a href="#menu"><span class="idx" aria-hidden="true">02</span> Dropdown Menu</a>
      <a href="#tabs"><span class="idx" aria-hidden="true">03</span> Tabs</a>
      <a href="#combobox"><span class="idx" aria-hidden="true">04</span> Combobox</a>
      <a href="#accordion"><span class="idx" aria-hidden="true">05</span> Accordion</a>
    </nav>
    <p class="side-note">WAI-ARIA APG patterns · vanilla JS · zero dependencies</p>
  </aside>

  <main id="main-content" tabindex="-1">
    <header class="page-head">
      <p class="eyebrow">Component library · Accessibility showcase</p>
      <h1>Accessible components, done right</h1>
      <p class="lede">Five core widgets built to the WAI-ARIA Authoring Practices. Every example is fully operable with the keyboard alone, and every ARIA attribute stays in lockstep with what you see. Press <kbd>Tab</kbd> and start exploring.</p>
    </header>

    <!-- ════════ 01 · Modal dialog ════════ -->
    <section id="modal" class="component" aria-labelledby="modal-h">
      <header class="component-head">
        <div>
          <p class="eyebrow">01 · Dialog</p>
          <h2 id="modal-h">Modal Dialog</h2>
        </div>
        <code class="pattern-tag">role="dialog" · aria-modal</code>
      </header>
      <p class="lede">A dialog that traps focus, makes the page behind it inert, and returns focus to the trigger on close. Escape and the backdrop both dismiss it.</p>
      <div class="demo">
        <button type="button" class="btn btn-primary" id="modal-trigger">Open dialog</button>
        <p class="status" id="modal-status" role="status"></p>
      </div>
      <p class="caption"><kbd>Enter</kbd> open · <kbd>Tab</kbd>/<kbd>Shift</kbd>+<kbd>Tab</kbd> cycle within · <kbd>Esc</kbd> close, focus returns to trigger</p>
    </section>

    <!-- ════════ 02 · Dropdown menu ════════ -->
    <section id="menu" class="component" aria-labelledby="menu-h">
      <header class="component-head">
        <div>
          <p class="eyebrow">02 · Menu</p>
          <h2 id="menu-h">Dropdown Menu</h2>
        </div>
        <code class="pattern-tag">role="menu" · roving tabindex</code>
      </header>
      <p class="lede">The button announces itself with <code>aria-haspopup="menu"</code>. Arrow keys wrap, Home/End jump, and selecting an item performs a visible action.</p>
      <div class="demo">
        <div class="menu-wrap">
          <button type="button" class="btn" id="menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-list">
            Actions
            <svg class="chev" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6.5l4 4 4-4"/></svg>
          </button>
          <div class="menu" id="menu-list" role="menu" aria-label="Actions" aria-orientation="vertical" hidden>
            <button type="button" class="menu-item" role="menuitem" tabindex="-1">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><rect x="2.75" y="2.75" width="10.5" height="10.5" rx="2"/><path d="M8 5.5v5M5.5 8h5"/></svg>
              New file
            </button>
            <button type="button" class="menu-item" role="menuitem" tabindex="-1">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><circle cx="12.25" cy="3.75" r="1.9"/><circle cx="3.75" cy="8" r="1.9"/><circle cx="12.25" cy="12.25" r="1.9"/><path d="M5.4 7.2l5.2-2.7M5.4 8.8l5.2 2.7"/></svg>
              Share…
            </button>
            <button type="button" class="menu-item" role="menuitem" tabindex="-1">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.3 2.8l1.9 1.9L5.4 12.5l-2.6.9.9-2.6 7.6-8z"/></svg>
              Rename
            </button>
            <div class="menu-sep" aria-hidden="true"></div>
            <button type="button" class="menu-item danger" role="menuitem" tabindex="-1">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M3 4.5h10M6.5 4.5V3h3v1.5M4.75 4.5l.6 8.5h5.3l.6-8.5"/></svg>
              Delete
            </button>
          </div>
        </div>
        <p class="status" id="menu-status" role="status"></p>
      </div>
      <p class="caption"><kbd>↓</kbd> open · <kbd>↑</kbd>/<kbd>↓</kbd> navigate, wraps · <kbd>Home</kbd>/<kbd>End</kbd> jump · <kbd>Enter</kbd> select · <kbd>Esc</kbd> close, focus returns</p>
    </section>

    <!-- ════════ 03 · Tabs ════════ -->
    <section id="tabs" class="component" aria-labelledby="tabs-h">
      <header class="component-head">
        <div>
          <p class="eyebrow">03 · Tabs</p>
          <h2 id="tabs-h">Tabs</h2>
        </div>
        <code class="pattern-tag">role="tablist"</code>
      </header>
      <p class="lede">Exactly one tab in the tab order at a time. Arrow keys move and activate; hidden panels carry the <code>hidden</code> attribute so they never enter the accessibility tree.</p>
      <div class="demo">
        <div class="tablist" role="tablist" id="tablist" aria-label="Component documentation">
          <button type="button" class="tab" role="tab" id="tab-1" aria-selected="true" aria-controls="tabpanel-1" tabindex="0">Overview</button>
          <button type="button" class="tab" role="tab" id="tab-2" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">Usage</button>
          <button type="button" class="tab" role="tab" id="tab-3" aria-selected="false" aria-controls="tabpanel-3" tabindex="-1">Accessibility</button>
        </div>
        <div class="tabpanel" role="tabpanel" id="tabpanel-1" aria-labelledby="tab-1" tabindex="0">
          <p>AriaKit ships five core widgets, each a single self-contained pattern from the WAI-ARIA Authoring Practices. No state is visual-only — every open, selected, or expanded condition is mirrored in ARIA attributes, and every widget is reachable with the keyboard alone.</p>
          <div class="chips" aria-hidden="true">
            <span class="chip">role=tablist</span>
            <span class="chip">roving tabindex</span>
            <span class="chip">hidden panels</span>
          </div>
        </div>
        <div class="tabpanel" role="tabpanel" id="tabpanel-2" aria-labelledby="tab-2" tabindex="0" hidden>
          <pre><code>&lt;div role="tablist" aria-label="Docs"&gt;
  &lt;button role="tab" id="tab-1" aria-selected="true"
          aria-controls="panel-1" tabindex="0"&gt;Overview&lt;/button&gt;
  &lt;button role="tab" id="tab-2" aria-selected="false"
          aria-controls="panel-2" tabindex="-1"&gt;Usage&lt;/button&gt;
&lt;/div&gt;
&lt;div role="tabpanel" id="panel-1"
     aria-labelledby="tab-1" tabindex="0"&gt;…&lt;/div&gt;</code></pre>
        </div>
        <div class="tabpanel" role="tabpanel" id="tabpanel-3" aria-labelledby="tab-3" tabindex="0" hidden>
          <ul class="attr-list">
            <li><code>role="tablist"</code> groups the tabs and announces the set.</li>
            <li><code>aria-selected</code> + <code>tabindex</code> keep exactly one tab in the tab order.</li>
            <li><code>aria-controls</code> ↔ <code>aria-labelledby</code> link each tab to its panel.</li>
            <li><code>hidden</code> removes closed panels from the accessibility tree.</li>
            <li>Arrow keys move and activate; <code>Home</code>/<code>End</code> jump.</li>
          </ul>
        </div>
      </div>
      <p class="caption"><kbd>←</kbd>/<kbd>→</kbd> move &amp; activate · <kbd>Home</kbd>/<kbd>End</kbd> jump · <kbd>Tab</kbd> into panel content</p>
    </section>

    <!-- ════════ 04 · Combobox ════════ -->
    <section id="combobox" class="component" aria-labelledby="combobox-h">
      <header class="component-head">
        <div>
          <p class="eyebrow">04 · Input</p>
          <h2 id="combobox-h">Combobox</h2>
        </div>
        <code class="pattern-tag">aria-activedescendant</code>
      </header>
      <p class="lede">The listbox is never focusable — the input owns focus and tracks the highlighted option by ID. Typing filters, and <kbd>Esc</kbd> reverts to the value the list opened with.</p>
      <div class="demo">
        <div class="cb-wrap">
          <label class="cb-label" for="cb-input">Programming language</label>
          <input class="cb-input" id="cb-input" type="text" role="combobox"
                 aria-expanded="false" aria-controls="cb-listbox" aria-autocomplete="list"
                 placeholder="Type to search — try 'py'" autocomplete="off" spellcheck="false">
          <ul class="cb-listbox" id="cb-listbox" role="listbox" aria-label="Programming languages" hidden></ul>
        </div>
        <p class="status" id="cb-status" role="status">Nothing selected yet.</p>
      </div>
      <p class="caption"><kbd>Type</kbd> filter · <kbd>↓</kbd>/<kbd>↑</kbd> highlight, wraps · <kbd>Enter</kbd> select · <kbd>Esc</kbd> revert &amp; close</p>
    </section>

    <!-- ════════ 05 · Accordion ════════ -->
    <section id="accordion" class="component" aria-labelledby="accordion-h">
      <header class="component-head">
        <div>
          <p class="eyebrow">05 · Disclosure</p>
          <h2 id="accordion-h">Accordion</h2>
        </div>
        <code class="pattern-tag">aria-expanded</code>
      </header>
      <p class="lede">Buttons toggle regions; several may be open at once. Height animates via the Web Animations API while <code>hidden</code> is applied only at the end of the animation.</p>
      <div class="demo">
        <div class="acc">
          <div class="acc-item">
            <h3 class="acc-h">
              <button type="button" class="acc-btn" id="acc-btn-1" aria-expanded="true" aria-controls="acc-panel-1" data-acc-btn>
                Keyboard contract
                <svg class="chev" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6.5l4 4 4-4"/></svg>
              </button>
            </h3>
            <div class="acc-region" id="acc-panel-1" role="region" aria-labelledby="acc-btn-1">
              <div class="acc-inner">
                <p><kbd>Enter</kbd> and <kbd>Space</kbd> toggle the region; focus stays on the button, and the content becomes reachable by <kbd>Tab</kbd> while open. Multiple regions may be open at the same time.</p>
                <p><a class="link" href="https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/">Read the APG disclosure pattern</a></p>
              </div>
            </div>
          </div>
          <div class="acc-item">
            <h3 class="acc-h">
              <button type="button" class="acc-btn" id="acc-btn-2" aria-expanded="false" aria-controls="acc-panel-2" data-acc-btn>
                Screen reader behavior
                <svg class="chev" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6.5l4 4 4-4"/></svg>
              </button>
            </h3>
            <div class="acc-region" id="acc-panel-2" role="region" aria-labelledby="acc-btn-2" hidden>
              <div class="acc-inner">
                <p>Each region uses <code>role="region"</code> with <code>aria-labelledby</code> pointing at its button, so the name is announced when the region is expanded. <code>aria-expanded</code> changes before the animation starts, keeping the state honest.</p>
                <p><a class="link" href="https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/">See the ARIA spec for regions</a></p>
              </div>
            </div>
          </div>
          <div class="acc-item">
            <h3 class="acc-h">
              <button type="button" class="acc-btn" id="acc-btn-3" aria-expanded="false" aria-controls="acc-panel-3" data-acc-btn>
                Reduced motion
                <svg class="chev" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6.5l4 4 4-4"/></svg>
              </button>
            </h3>
            <div class="acc-region" id="acc-panel-3" role="region" aria-labelledby="acc-btn-3" hidden>
              <div class="acc-inner">
                <p>When <code>prefers-reduced-motion: reduce</code> is set, regions toggle instantly — no height animation, no transitions anywhere on the page.</p>
                <p><button type="button" class="btn" id="acc-replay">Replay this animation</button></p>
              </div>
            </div>
          </div>
        </div>
      </div>
      <p class="caption"><kbd>Enter</kbd>/<kbd>Space</kbd> toggle · multiple open at once · <kbd>Tab</kbd> into open content</p>
    </section>

    <footer class="page-foot">
      <p>Built with vanilla HTML, CSS, and JavaScript — no dependencies, no build step. Patterns follow <a href="https://www.w3.org/WAI/ARIA/apg/">WAI-ARIA Authoring Practices 1.2</a>.</p>
    </footer>
  </main>
</div>

<!-- Modal lives outside <main> so the page can be made inert while it is open -->
<div id="modal-root" hidden>
  <div class="modal-backdrop" data-modal-backdrop></div>
  <div class="modal" id="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc" tabindex="-1">
    <h2 id="dialog-title">Publish AriaKit v2.4</h2>
    <p class="desc" id="dialog-desc">This makes the release available to every project that pins this library. You can unpublish within 24 hours.</p>
    <label class="field"><input type="checkbox" id="modal-notify"> Notify all subscribers</label>
    <div class="actions">
      <button type="button" class="btn" id="modal-cancel">Cancel</button>
      <button type="button" class="btn btn-primary" id="modal-confirm">Publish</button>
    </div>
  </div>
</div>

<div id="sr-live" class="sr-only" role="status" aria-live="polite"></div>

<script>
'use strict';

/* ═══════════════════════════════════════════════════════════
   CONFIG — tunable constants
═══════════════════════════════════════════════════════════ */
const CONFIG = {
  durations: {
    accordion: 260, // ms — accordion region height animation
  },
  combobox: {
    maxVisible: 8, // max options rendered in the listbox
  },
  nav: {
    rootMargin: '-25% 0px -60% 0px', // sidebar active-section tracking band
  },
};

/* ═══════════════════════════════════════════════════════════
   Utilities
═══════════════════════════════════════════════════════════ */
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));

const prefersReducedMotion = () =>
  window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/** Focusable, visible, non-inert elements inside `root` (DOM order). */
function focusableElements(root) {
  return $$('a[href], button, input, select, textarea, [tabindex]', root).filter(el =>
    !el.inert &&
    !el.disabled &&
    el.getAttribute('tabindex') !== '-1' &&
    el.getClientRects().length > 0
  );
}

/* Shared polite live region (kept out of <main> so it survives inert). */
const srLive = $('#sr-live');
function announce(message) {
  srLive.textContent = '';
  window.setTimeout(() => { srLive.textContent = message; }, 30);
}

/* ═══════════════════════════════════════════════════════════
   Focus-trap utility
   Keeps Tab / Shift+Tab cycling inside `container` while active.
   Returns a dispose function.
═══════════════════════════════════════════════════════════ */
function trapFocus(container, { onEscape } = {}) {
  const onKeydown = (event) => {
    if (event.key === 'Escape' && onEscape) {
      onEscape(event);
      return;
    }
    if (event.key !== 'Tab') return;
    const focusables = focusableElements(container);
    if (focusables.length === 0) {
      event.preventDefault();
      container.focus();
      return;
    }
    const first = focusables[0];
    const last = focusables[focusables.length - 1];
    const active = document.activeElement;
    const inside = container.contains(active);
    if (event.shiftKey) {
      if (!inside || active === first || active === container) {
        event.preventDefault();
        last.focus();
      }
    } else if (!inside || active === last) {
      event.preventDefault();
      first.focus();
    }
  };
  document.addEventListener('keydown', onKeydown, true);
  return () => document.removeEventListener('keydown', onKeydown, true);
}

/* ═══════════════════════════════════════════════════════════
   01 · Modal dialog
   role=dialog · aria-modal · focus trap · inert page behind
═══════════════════════════════════════════════════════════ */
const modal = (() => {
  const root = $('#modal-root');
  const dialog = $('#dialog');
  const backdrop = $('[data-modal-backdrop]', root);
  const trigger = $('#modal-trigger');
  const status = $('#modal-status');
  const cancelBtn = $('#modal-cancel');
  const confirmBtn = $('#modal-confirm');
  const notify = $('#modal-notify');
  const pageMain = $('#main-content');
  const sidebar = $('.sidebar');

  let lastTrigger = null;
  let releaseTrap = null;

  function open() {
    if (!root.hidden) return;
    lastTrigger = document.activeElement;
    notify.checked = false;
    root.hidden = false;
    pageMain.inert = true;   // page behind is not focusable
    sidebar.inert = true;
    releaseTrap = trapFocus(root, { onEscape: close });
    const first = focusableElements(dialog)[0] || dialog;
    first.focus();
    status.textContent = 'Dialog opened.';
    announce('Dialog opened: ' + $('#dialog-title').textContent);
  }

  function close() {
    if (root.hidden) return;
    root.hidden = true;
    pageMain.inert = false;
    sidebar.inert = false;
    if (releaseTrap) { releaseTrap(); releaseTrap = null; }
    if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
    else dialog.focus();
    status.textContent = '';
    announce('Dialog closed.');
  }

  trigger.addEventListener('click', open);
  cancelBtn.addEventListener('click', close);
  confirmBtn.addEventListener('click', () => {
    close();
    status.textContent = 'AriaKit v2.4 published ✓';
  });
  backdrop.addEventListener('click', close);

  return { open, close, isOpen: () => !root.hidden, dialog, trigger };
})();

/* ═══════════════════════════════════════════════════════════
   02 · Dropdown menu
   aria-haspopup=menu · roving tabindex over role=menuitem
═══════════════════════════════════════════════════════════ */
const menu = (() => {
  const btn = $('#menu-btn');
  const list = $('#menu-list');
  const items = $$('.menu-item', list);
  const status = $('#menu-status');
  let open = false;

  function setExpanded(v) { btn.setAttribute('aria-expanded', String(v)); }

  function openMenu(initialFocus) {
    if (open) return;
    open = true;
    list.hidden = false;
    setExpanded(true);
    (initialFocus || items[0]).focus();
  }

  function closeMenu(returnFocus = true) {
    if (!open) return;
    open = false;
    list.hidden = true;
    setExpanded(false);
    if (returnFocus) btn.focus();
  }

  btn.addEventListener('click', () => (open ? closeMenu() : openMenu()));
  btn.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); openMenu(); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); openMenu(items[items.length - 1]); }
  });

  list.addEventListener('keydown', (e) => {
    const i = items.indexOf(document.activeElement);
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); items[(i + 1) % items.length].focus(); break;
      case 'ArrowUp':   e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); break;
      case 'Home':      e.preventDefault(); items[0].focus(); break;
      case 'End':       e.preventDefault(); items[items.length - 1].focus(); break;
      case 'Escape':    e.preventDefault(); closeMenu(); break;
      case 'Tab':       closeMenu(); break; // no preventDefault — focus moves on naturally
    }
  });

  const actions = {
    'New file': () => 'Created “Untitled-1”',
    'Share…':   () => 'Share link copied to clipboard',
    'Rename':   () => 'Rename field would appear here',
    'Delete':   () => 'Item deleted',
  };
  items.forEach(item => item.addEventListener('click', () => {
    const label = item.textContent.trim();
    status.textContent = actions[label] ? actions[label]() : label;
    closeMenu();
  }));

  document.addEventListener('pointerdown', (e) => {
    if (open && !list.contains(e.target) && !btn.contains(e.target)) closeMenu(false);
  });

  return { isOpen: () => open, btn, list, items };
})();

/* ═══════════════════════════════════════════════════════════
   03 · Tabs
   roving tabindex · arrow keys move & activate · hidden panels
═══════════════════════════════════════════════════════════ */
const tabs = (() => {
  const tablist = $('#tablist');
  const tabButtons = $$('.tab', tablist);

  function activate(tab, focus = true) {
    tabButtons.forEach(t => {
      const selected = t === tab;
      t.setAttribute('aria-selected', String(selected));
      t.tabIndex = selected ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !selected;
    });
    if (focus) tab.focus();
  }

  tablist.addEventListener('keydown', (e) => {
    const i = tabButtons.indexOf(document.activeElement);
    if (i === -1) return;
    let next = null;
    if (e.key === 'ArrowRight') next = tabButtons[(i + 1) % tabButtons.length];
    else if (e.key === 'ArrowLeft') next = tabButtons[(i - 1 + tabButtons.length) % tabButtons.length];
    else if (e.key === 'Home') next = tabButtons[0];
    else if (e.key === 'End') next = tabButtons[tabButtons.length - 1];
    if (next) { e.preventDefault(); activate(next); }
  });

  tabButtons.forEach(t => t.addEventListener('click', () => activate(t)));

  return { tabButtons, activate };
})();

/* ═══════════════════════════════════════════════════════════
   04 · Combobox (autocomplete)
   aria-activedescendant pattern — the listbox is never focusable
═══════════════════════════════════════════════════════════ */
const combobox = (() => {
  const input = $('#cb-input');
  const listbox = $('#cb-listbox');
  const status = $('#cb-status');
  const OPTIONS = [
    'Ada', 'Assembly', 'Bash', 'C', 'C#', 'C++', 'Clojure', 'Dart',
    'Elixir', 'Erlang', 'Go', 'Haskell', 'Java', 'JavaScript', 'Kotlin',
    'Lua', 'Objective-C', 'PHP', 'Python', 'R', 'Ruby', 'Rust',
    'Scala', 'Swift', 'TypeScript',
  ];

  let open = false;
  let options = [];      // currently rendered option names
  let activeIndex = -1;  // index into `options`
  let valueAtOpen = '';  // value to restore on Escape

  function render(matches) {
    options = matches.slice(0, CONFIG.combobox.maxVisible);
    listbox.innerHTML = '';
    if (options.length === 0) {
      const li = document.createElement('li');
      li.className = 'cb-empty';
      li.textContent = 'No matches';
      listbox.appendChild(li);
    } else {
      options.forEach((name, i) => {
        const li = document.createElement('li');
        li.id = 'cb-opt-' + i;
        li.className = 'cb-option';
        li.setAttribute('role', 'option');
        li.textContent = name;
        li.addEventListener('mousedown', (e) => e.preventDefault()); // keep focus on the input
        li.addEventListener('click', () => select(i));
        listbox.appendChild(li);
      });
    }
  }

  function setActive(i) {
    activeIndex = i;
    Array.from(listbox.children).forEach((li, idx) => {
      if (li.classList) li.classList.toggle('active', idx === i);
    });
    const li = listbox.children[i];
    if (li && li.id) {
      input.setAttribute('aria-activedescendant', li.id);
      li.scrollIntoView({ block: 'nearest' });
    } else {
      input.removeAttribute('aria-activedescendant');
    }
  }

  function update() {
    const q = input.value.trim().toLowerCase();
    render(OPTIONS.filter(o => o.toLowerCase().includes(q)));
    const selIndex = options.indexOf(input.value);
    setActive(selIndex > -1 ? selIndex : (options.length ? 0 : -1));
  }

  function openList() {
    if (open) { update(); return; }
    open = true;
    valueAtOpen = input.value;
    listbox.hidden = false;
    input.setAttribute('aria-expanded', 'true');
    update();
  }

  function closeList() {
    if (!open) return;
    open = false;
    listbox.hidden = true;
    input.setAttribute('aria-expanded', 'false');
    activeIndex = -1;
    input.removeAttribute('aria-activedescendant');
  }

  function select(i) {
    const name = options[i];
    input.value = name;
    closeList();
    status.textContent = 'Selected: ' + name;
    announce('Selected ' + name);
  }

  input.addEventListener('input', openList);
  input.addEventListener('click', () => { if (!open) openList(); });

  input.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (!open) openList();
      else if (options.length) setActive((activeIndex + 1) % options.length);
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (!open) openList();
      else if (options.length) setActive((activeIndex - 1 + options.length) % options.length);
    } else if (e.key === 'Enter') {
      if (open && activeIndex > -1) { e.preventDefault(); select(activeIndex); }
    } else if (e.key === 'Escape') {
      if (open) {
        e.preventDefault();
        input.value = valueAtOpen;
        closeList();
      }
    } else if (e.key === 'Tab') {
      closeList();
    }
  });

  input.addEventListener('blur', closeList);

  return { input, listbox, isOpen: () => open, options: () => options.slice() };
})();

/* ═══════════════════════════════════════════════════════════
   05 · Accordion
   aria-expanded buttons · height animation · hidden only at end
═══════════════════════════════════════════════════════════ */
const accordion = (() => {
  const buttons = $$('[data-acc-btn]');
  const DUR = CONFIG.durations.accordion;

  function toggle(btn) {
    const region = document.getElementById(btn.getAttribute('aria-controls'));
    const expanding = btn.getAttribute('aria-expanded') === 'false';
    btn.setAttribute('aria-expanded', String(expanding)); // state first, visuals second

    if (expanding) {
      region.hidden = false;
      if (prefersReducedMotion()) { region.style.height = ''; return; }
      const to = region.scrollHeight;
      region.style.height = '0px';
      const anim = region.animate(
        [{ height: '0px' }, { height: to + 'px' }],
        { duration: DUR, easing: 'cubic-bezier(0.25, 0.8, 0.35, 1)' }
      );
      anim.onfinish = () => { region.style.height = ''; };
    } else {
      if (prefersReducedMotion()) { region.hidden = true; region.style.height = ''; return; }
      const from = region.scrollHeight;
      region.style.height = from + 'px';
      const anim = region.animate(
        [{ height: from + 'px' }, { height: '0px' }],
        { duration: DUR, easing: 'cubic-bezier(0.4, 0.1, 0.3, 1)' }
      );
      anim.onfinish = () => { region.hidden = true; region.style.height = ''; };
    }
  }

  buttons.forEach(btn => btn.addEventListener('click', () => toggle(btn)));

  const replay = $('#acc-replay');
  const replayBtn = buttons[2];
  if (replay) {
    replay.addEventListener('click', () => {
      const region = document.getElementById(replayBtn.getAttribute('aria-controls'));
      if (region.hidden) { toggle(replayBtn); return; }
      toggle(replayBtn); // collapse
      window.setTimeout(() => toggle(replayBtn), prefersReducedMotion() ? 30 : DUR + 60); // then re-expand
    });
  }

  return { buttons };
})();

/* ═══════════════════════════════════════════════════════════
   Sidebar — active section tracking
═══════════════════════════════════════════════════════════ */
const nav = (() => {
  const links = $$('.side-nav a');
  const setActive = (link) => links.forEach(a => a.classList.toggle('active', a === link));
  const io = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const link = links.find(a => a.getAttribute('href').slice(1) === entry.target.id);
        if (link) setActive(link);
      }
    });
  }, { rootMargin: CONFIG.nav.rootMargin });
  links.forEach(a => {
    const section = document.getElementById(a.getAttribute('href').slice(1));
    if (section) io.observe(section);
  });
  setActive(links[0]);
  return { links };
})();

/* ═══════════════════════════════════════════════════════════
   ARIA integrity self-check
   Every referenced ID must resolve; tab ↔ panel wiring must hold.
═══════════════════════════════════════════════════════════ */
function checkAriaIntegrity() {
  const problems = [];
  ['aria-controls', 'aria-labelledby', 'aria-describedby'].forEach(attr => {
    document.querySelectorAll(`[${attr}]`).forEach(el => {
      el.getAttribute(attr).split(/\s+/).filter(Boolean).forEach(id => {
        if (!document.getElementById(id)) {
          problems.push(`${attr} on <${el.tagName.toLowerCase()} id="${el.id || '(anon)'}"> → missing #${id}`);
        }
      });
    });
  });
  document.querySelectorAll('[role="tab"]').forEach(tab => {
    const panel = document.getElementById(tab.getAttribute('aria-controls'));
    if (!panel || panel.getAttribute('aria-labelledby') !== tab.id) {
      problems.push(`tab #${tab.id} ↔ panel wiring broken`);
    }
  });
  return { ok: problems.length === 0, problems };
}

/* Debug / test API */
window.__ariaKit = {
  config: CONFIG,
  modal, menu, tabs, combobox, accordion,
  check: checkAriaIntegrity,
  focusableElements,
};

const integrity = checkAriaIntegrity();
if (integrity.ok) {
  console.info('[AriaKit] ARIA integrity check passed — all referenced IDs resolve.');
} else {
  console.warn('[AriaKit] ARIA integrity problems:', integrity.problems);
}
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"37ca022c-cb9e-43c7-9e80-1cc15a175a78","tokensIn":1318830,"tokensOut":85474,"tokensTotal":1404304,"cost":0,"turns":31,"toolCalls":36,"failedToolCalls":1,"timestamp":"2026-09-01T22:45:30.250Z"} -->