Accessible Components
lfm2 24b a2b · basic

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"} -->