← Accessible Components results

Accessible Components

lfm2 24b a2b · basic

Initial view of Accessible Components

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 16,630 bytes · SHA-256 efe0ebc844be
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Component Showcase</title>
<style>
:root {
  --accent: #2c3e50;
  --accent-hover: #3498db;
  --bg: #f9fafb;
  --card-bg: #ffffff;
  --border: #e0e6ed;
  --text: #333333;
  --focus: #2c3e50;
  --focus-ring: 3px solid #3498db;
  --radius: 8px;
  --spacing: 1.5rem;
  --transition: all 0.2s ease;
  --shadow: 0 4px 12px rgba(0,0,0,.05);
}
* { box-sizing: border-box; }
body {
  font-family: system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  margin: 0;
  padding: 0;
}
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--accent);
  color: white;
  padding: .5rem 1rem;
  z-index: 100;
  text-decoration: none;
  border-radius: var(--radius);
}
.skip-link:focus { top: 0; }
main {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--spacing);
}
nav.sidebar {
  position: sticky;
  top: var(--spacing);
  width: 220px;
  background: var(--card-bg);
  border: var(--border);
  padding: var(--spacing);
  box-shadow: var(--shadow);
}
nav a {
  display: block;
  color: var(--accent);
  text-decoration: none;
  margin-bottom: .5rem;
  padding: .25rem 0;
  border-radius: var(--radius);
  transition: var(--transition);
}
nav a:hover, nav a:focus { background: var(--accent-hover); color: white; }
section {
  margin-bottom: 2rem;
  background: var(--card-bg);
  padding: var(--spacing);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
h1 { margin-top: 0; }
h2 { margin-top: 1.5rem; margin-bottom: .75rem; }
.caption {
  font-size: .85rem;
  color: #666;
  margin-top: .25rem;
}
.modal {
  position: fixed; top:0; left:0; right:0; bottom:0;
  background: rgba(0,0,0,.4);
  display:flex; align-items:center; justify-content:center;
  z-index: 10;
}
.modal-dialog {
  background: var(--card-bg);
  border-radius: var(--radius);
  padding: var(--spacing);
  min-width: 300px;
  max-width: 90vw;
  outline: none;
}
.modal-header { font-weight: bold; margin-bottom: .5rem; }
.modal-body { margin-bottom: .75rem; }
.modal-close {
  background: none; border: none; color: var(--accent); cursor: pointer;
  font-size: 1.5rem; text-align: right;
}
.backdrop { position: fixed; inset:0; background: rgba(0,0,0,.4); z-index: 9; }
.tablist { display:flex; gap: .5rem; margin-bottom:.75rem; }
.tab { padding:.25rem .75rem; border: none; background: var(--card-bg); cursor: pointer; border-radius: var(--radius); font-weight: 600; }
.tab[aria-selected="true"] { background: var(--accent-hover); color: white; }
.tablist input { display:none; }
.tabpanel { display: none; padding: .75rem 0; }
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to main content</a>
<main id="main">
  <section>
    <h1>Accessible Component Showcase</h1>
    <p>All components are keyboard and screen-reader accessible.</p>
  </section>

  <!-- Modal -->
  <section>
    <h2>1. Modal Dialog</h2>
    <button id="modal-trigger" aria-controls="modal-dialog" aria-haspopup="dialog">Open Modal</button>
    <div class="modal" id="modal" hidden>
      <div class="backdrop" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="modal-title">
        <div class="modal-dialog" role="document" aria-labelledby="modal-title">
          <h3 id="modal-title" class="modal-header">Modal Title</h3>
          <p>This is a dialog. Tab cycles through, Esc closes, click backdrop closes.</p>
          <button class="modal-close" aria-label="Close modal">×</button>
        </div>
      </div>
    </div>
  </section>

  <!-- Dropdown -->
  <section>
    <h2>2. Dropdown Menu</h2>
    <button id="dropdown-trigger" aria-haspopup="menu" aria-expanded="false">Toggle Menu</button>
    <ul id="dropdown-menu" class="dropdown" role="menu" hidden>
      <li role="menuitem" tabindex="-1" data-action="option1">Option 1</li>
      <li role="menuitem" tabindex="-1" data-action="option2">Option 2</li>
      <li role="menuitem" tabindex="-1" data-action="option3">Option 3</li>
    </ul>
  </section>

  <!-- Tabs -->
  <section>
    <h2>3. Tabs</h2>
    <div class="tablist" role="tablist">
      <button id="tab1" role="tab" aria-selected="true" aria-controls="tabpanel-1" tabindex="0">Tab 1</button>
      <button id="tab2" role="tab" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">Tab 2</button>
      <button id="tab3" role="tab" aria-selected="false" aria-controls="tabpanel-3" tabindex="-1">Tab 3</button>
    </div>
    <div id="tabpanel-1" role="tabpanel" aria-labelledby="tab1" tabindex="0">
      <h4>Tab 1 Content</h4>
      <p>This is the first tab.</p>
    </div>
    <div id="tabpanel-2" role="tabpanel" aria-labelledby="tab2" hidden>
      <h4>Tab 2 Content</h4>
      <p>This is the second tab.</p>
    </div>
    <div id="tabpanel-3" role="tabpanel" aria-labelledby="tab3" hidden>
      <h4>Tab 3 Content</h4>
      <p>This is the third tab.</p>
    </div>
  </section>

  <!-- Combobox -->
  <section>
    <h2>4. ComboBox (Autocomplete)</h2>
    <label for="autocomplete">Search: <input id="autocomplete" type="text" aria-expanded="false" aria-controls="listbox" aria-activedescendant="" aria-label="Select an option"></label>
    <ul id="listbox" role="listbox" hidden>
      <li role="option" tabindex="-1" data-value="a">A</li>
      <li role="option" tabindex="-1" data-value="b">B</li>
      <li role="option" tabindex="-1" data-value="c">C</li>
      <li role="option" tabindex="-1" data-value="d">D</li>
      <li role="option" tabindex="-1" data-value="e">E</li>
    </ul>
  </section>

  <!-- Accordion -->
  <section>
    <h2>5. Accordion / Disclosure Group</h2>
    <button id="accordion-btn" aria-expanded="false" aria-controls="panel1" tabindex="0">Section 1</button>
    <div id="panel1" role="region" hidden>
      <h4>Section 1</h4>
      <p>Open/closed with Enter/Space.</p>
    </div>
    <button id="accordion-btn2" aria-expanded="false" aria-controls="panel2" tabindex="-1">Section 2</button>
    <div id="panel2" role="region" hidden>
      <h4>Section 2</h4>
      <p>Multiple open allowed.</p>
    </div>
  </section>
</main>

<script>
/* === CONSTANTS === */
const ANIMATION = { fade: "fade" };
const COLORS = { primary: "#3498db", bg: "#f9fafb", card: "#fff", text: "#222", border: "#eee", focus: "#3498db" };
const SPACING = { m: "1.5rem", s: ".5rem" };
const FOCUS_RADIUS = 8;

/* === UTILS === */
function focusTrap(container, options) {
  const { open, onClose, onFocus, onBlur } = options;
  let active = null;
  const focusable = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const first = focusable[0];
  const last = focusable[focusable.length-1];

  function moveFocus(e) {
    if (e.shiftKey) {
      active = (active === first ? last : active).previousElementSibling;
    } else {
      active = (active === last ? first : active).nextElementSibling;
    }
    if (active && active !== container) focus(active);
  }
  function trap(e) {
    const isTab = e.keyCode === 39 || e.key === "Tab";
    if (!isTab) return;
    if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); return; }
    if (!isShiftKey && active === last) { e.preventDefault(); first.focus(); return; }
    if (active && !active.contains(e.target)) focus(e.target);
  }
  function onFocus(e) {
    active = e.target;
    if (open && !focusable.includes(active)) focus(focusable[0]);
    onFocus?.(active);
  }
  function onBlur(e) {
    if (!open && active === first) first.focus();
    if (active === last && !open) last.focus();
    onBlur?.(active);
  }
  container.addEventListener('keydown', e => {
    if (e.keyCode === 39) { // Tab
      moveFocus(e);
      e.preventDefault();
    } else if (e.keyCode === 32 || e.key === "Escape") {
      onClose?.(active);
    } else if (e.keyCode === 13 || e.key === "Enter") {
      if (active && focusable.includes(active)) {
        const el = active;
        if (el.getAttribute('role') === 'menuitem') {
          onFocus?.(el);
        } else if (el.id === options.controls) {
          options.onFocus?.(el);
        } else if (el.id === options.controls + "-btn") {
          options.onClick?.(el);
        }
      }
    } else if (e.keyCode === 40) { // Arrow
      const items = focusable;
      let idx = items.indexOf(active || items[0]);
      if (e.keyCode === 38) { // Up
        idx = (idx - 1 + items.length) % items.length; idx = items[idx];
      } else if (e.keyCode === 39) { // Down
        idx = (idx + 1) % items.length; idx = items[idx];
      } else if (e.keyCode === 40 || e.key === "ArrowDown") {
        active = items[idx];
        if (active && active !== container) focus(active);
      }
      e.preventDefault();
    }
  });
  container.addEventListener('focus', onFocus, { once: true });
  container.addEventListener('blur', onBlur, { once: true });
  container.addEventListener('keydown', trap, { once: false });
  return { focus: focusTrap, onClose, onFocus, onBlur };
}

/* === COMPONENTS === */

// Modal
const modal = document.getElementById('modal');
const trigger = document.getElementById('modal-trigger');
const closeBtn = document.querySelector('.modal-close');
const backdrop = modal.querySelector('.backdrop');
let modalOpen = false, modalFocus;

function openModal() {
  modal.hidden = false;
  modal.setAttribute('aria-hidden', 'false');
  trigger.setAttribute('aria-expanded', 'true');
  modalFocus = trigger;
  document.body.style.overflow = 'hidden';
  modalOpen = true;
  // focus trap
  const trap = focusTrap(modal, { open: true, onClose, onFocus, onBlur });
  modal.addEventListener('keydown', e => {
    if (e.keyCode === 27) closeModal();
  });
  modal.addEventListener('focusin', e => {
    if (!modalOpen) return;
    modalFocus = e.target;
  });
  modal.addEventListener('focusout', e => {
    if (!modalOpen) return;
    if (modalFocus && !modal.contains(modalFocus)) modal.focus();
  });
  modal.focus();
}

function closeModal() {
  modal.hidden = true;
  modal.setAttribute('aria-hidden', 'true');
  trigger.setAttribute('aria-expanded', 'false');
  modalOpen = false;
  setTimeout(() => document.body.style.overflow = 'auto', 50);
}

// Dropdown
const dropdown = document.getElementById('dropdown-trigger');
const menu = document.getElementById('dropdown-menu');
let expanded = false, activeItem = null;

function toggleMenu() {
  expanded = !expanded;
  dropdown.setAttribute('aria-expanded', expanded.toString());
  if (expanded) {
    const items = menu.querySelectorAll('li');
    items.forEach(i => i.setAttribute('tabindex', '-1'));
    const first = menu.querySelector('li');
    if (first) first.setAttribute('tabindex', '0');
    activeItem = null;
    items.forEach(i => i.setAttribute('tabindex', ''));
    if (items.length) {
      items[0].focus();
      activeItem = items[0];
    }
  } else {
    menu.querySelectorAll('li').forEach(i => i.setAttribute('tabindex', '-1'));
    activeItem?.setAttribute('tabindex', '');
  }
}

dropdown.addEventListener('click', toggleMenu, false);
dropdown.addEventListener('keydown', e => {
  if (!expanded) return;
  const items = menu.querySelectorAll('li');
  if (e.keyCode === 39) { // Down
    activeItem = items[0];
    items.forEach(i => i.setAttribute('tabindex', ''));
    items.forEach(i => i.setAttribute('tabindex', '-1'));
    items[0].setAttribute('tabindex', '0');
    items[0].focus();
  } else if (e.keyCode === 32) { // Up
    activeItem = items[items.length-1];
    items.forEach(i => i.setAttribute('tabindex', ''));
    items.forEach(i => i.setAttribute('tabindex', '-1'));
    items[items.length-1].setAttribute('tabindex', '0');
    items[items.length-1].focus();
  } else if (e.keyCode === 27) closeMenu(); // Escape
});
function closeMenu() {
  toggleMenu();
}

// Tabs
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('[role="tabpanel"]');
let activeTab = null, activeIndex = -1;
function setActiveTab(idx) {
  tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
  panels.forEach(p => p.hidden = true);
  if (idx < 0 || idx >= tabs.length) return;
  const tab = tabs[idx];
  const panel = panels[idx];
  tab.setAttribute('aria-selected', 'true');
  tab.setAttribute('tabindex', '0');
  tab.classList.add('focus');
  panel.hidden = false;
  panel.setAttribute('hidden', '');
  activeTab = idx;
  activeIndex = idx;
  // focus trap
  const trap = focusTrap(panel, { open: true, onClose });
  panel.focus();
}

tabs.forEach((t,i) => {
  t.addEventListener('click', () => setActiveTab(i));
  t.addEventListener('keydown', e => {
    if (e.keyCode === 39) setActiveTab(i); // Enter
    if (e.keyCode === 32) setActiveTab(i); // Space
    if (e.keyCode === 27) closeTab(i);
  });
});
// Arrow keys
document.querySelector('.tablist').addEventListener('keydown', e => {
  if (!expanded) return;
  const idx = activeIndex;
  if (e.keyCode === 39) { // Down
    setActiveTab(idx+1);
  } else if (e.keyCode === 32) { // Up
    setActiveTab(idx-1);
  }
});

// Combobox
const combo = document.getElementById('autocomplete');
const listbox = document.getElementById('listbox');
let activeOption = null;
const options = Array.from(listbox.querySelectorAll('li')).map(li => li.getAttribute('data-value'));

function setActiveOption(val) {
  if (activeOption !== null) {
    const prev = document.querySelector(`[data-value="${activeOption}"]`);
    if (prev) prev.setAttribute('tabindex', '');
    prev.setAttribute('tabindex', '-1');
  }
  activeOption = val;
  const opt = listbox.querySelector(`[data-value="${val}"]`);
  if (opt) {
    opt.setAttribute('tabindex', '0');
    opt.focus();
    opt.classList.add('focus');
  }
}

combo.addEventListener('keydown', e => {
  if (!expanded) return;
  const opts = listbox.querySelectorAll('li');
  if (e.keyCode === 38) { // Up
    activeOption = opts[opts.length-1];
    setActiveOption(activeOption);
    opts[opts.length-1].setAttribute('tabindex', '0');
    opts[opts.length-1].focus();
  } else if (e.keyCode === 39) { // Down
    activeOption = opts[0];
    setActiveOption(activeOption);
    opts[0].setAttribute('tabindex', '0');
    opts[0].focus();
  } else if (e.keyCode === 32 || e.key === "Enter") {
    if (activeOption) {
      combo.value = activeOption;
      combo.setAttribute('aria-activedescendant', `${activeOption}`);
      listbox.removeAttribute('aria-activedescendant');
      closeCombobox();
    }
  } else if (e.keyCode === 27) closeCombobox();
});
combo.addEventListener('focus', e => {
  if (e.target === combo) {
    listbox.focus();
    setActiveOption(combo.value);
  }
});

// Accordion
const accordions = document.querySelectorAll('.accordion-btn, .accordion-btn2');
const panelsAcc = document.querySelectorAll('[role="region"]');
let openRegions = new Set();

function toggleAccordion(id) {
  const panel = document.getElementById(id.replace('accordion-', ''));
  const expanded = panel.hidden;
  panel.hidden = !expanded;
  const btn = document.getElementById(id);
  btn.setAttribute('aria-expanded', expanded.toString());
  if (expanded) {
    panel.style.height = '';
    panel.setAttribute('hidden', '');
  } else {
    panel.style.height = panel.scrollHeight + 'px';
    panel.removeAttribute('hidden');
  }
  openRegions = expanded ? new Set([id]) : new Set();
}

accordions.forEach(btn => {
  btn.addEventListener('click', () => toggleAccordion(btn.id));
  btn.addEventListener('keydown', e => {
    if (e.keyCode === 39 || e.key === "Enter") toggleAccordion(btn.id);
  });
});

/* === INIT === */
modal.addEventListener('keydown', e => { if (e.keyCode === 27) closeModal(); });
dropdown.addEventListener('keydown', e => {
  if (!expanded) return;
  if (e.keyCode === 39) { // Down
    const items = menu.querySelectorAll('li');
    items.forEach(i => i.setAttribute('tabindex', '-1'));
    items[0].setAttribute('tabindex', '0');
    items[0].focus();
    activeItem = items[0];
  } else if (e.keyCode === 32) { // Up
    const items = menu.querySelectorAll('li');
    items.forEach(i => i.setAttribute('tabindex', '-1'));
    items[items.length-1].setAttribute('tabindex', '0');
    items[items.length-1].focus();
    activeItem = items[items.length-1];
  }
});
setActiveTab(0);

/* === END === */
</script>
</body>
</html>
<!-- agent-meta {"model":"liquid/lfm2-24b-a2b","provider":"lmstudio","persona":"basic","sessionId":"b3c049fe-40c5-4a3e-883b-16b034ba1024","tokensIn":1439,"tokensOut":5063,"tokensTotal":6502,"cost":0,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-01T04:50:41.165Z"} -->