This guide shows practical examples of using the Attr plugin for building accessible, interactive components.
A fully accessible contact form with validation and error handling.
const AccessibleForm = {
setup({ signal }) {
const formData = signal({
name: "",
email: "",
message: ""
});
const errors = signal({});
const isSubmitting = signal(false);
const submitSuccess = signal(false);
const validate = () => {
const newErrors = {};
if (!formData.value.name.trim()) {
newErrors.name = "Name is required";
}
if (!formData.value.email.includes("@")) {
newErrors.email = "Valid email is required";
}
if (formData.value.message.length < 10) {
newErrors.message = "Message must be at least 10 characters";
}
errors.value = newErrors;
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
isSubmitting.value = true;
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
submitSuccess.value = true;
} catch (error) {
errors.value = { submit: "Failed to submit form" };
} finally {
isSubmitting.value = false;
}
};
const updateField = (field, value) => {
formData.value = { ...formData.value, [field]: value };
};
return { formData, errors, isSubmitting, submitSuccess, handleSubmit, updateField };
},
template({ formData, errors, isSubmitting, submitSuccess }) {
return `
<form @submit="handleSubmit" aria-label="Contact Form">
<!-- Success Message -->
<div
role="alert"
aria-live="polite"
hidden="${!submitSuccess.value}"
>
Form submitted successfully!
</div>
<!-- Name Field -->
<div class="form-group">
<label id="name-label" for="name">Name</label>
<input
type="text"
id="name"
name="name"
value="${formData.value.name}"
@input="updateField('name', $event.target.value)"
aria-labelledby="name-label"
aria-describedby="name-error"
aria-invalid="${errors.value.name ? 'true' : 'false'}"
aria-required="true"
required="${true}"
/>
<span
id="name-error"
role="alert"
class="error"
hidden="${!errors.value.name}"
>
${errors.value.name || ''}
</span>
</div>
<!-- Email Field -->
<div class="form-group">
<label id="email-label" for="email">Email</label>
<input
type="email"
id="email"
name="email"
value="${formData.value.email}"
@input="updateField('email', $event.target.value)"
aria-labelledby="email-label"
aria-describedby="email-error"
aria-invalid="${errors.value.email ? 'true' : 'false'}"
aria-required="true"
required="${true}"
/>
<span
id="email-error"
role="alert"
class="error"
hidden="${!errors.value.email}"
>
${errors.value.email || ''}
</span>
</div>
<!-- Message Field -->
<div class="form-group">
<label id="message-label" for="message">Message</label>
<textarea
id="message"
name="message"
value="${formData.value.message}"
@input="updateField('message', $event.target.value)"
aria-labelledby="message-label"
aria-describedby="message-error message-hint"
aria-invalid="${errors.value.message ? 'true' : 'false'}"
aria-required="true"
required="${true}"
></textarea>
<span id="message-hint" class="hint">
Minimum 10 characters
</span>
<span
id="message-error"
role="alert"
class="error"
hidden="${!errors.value.message}"
>
${errors.value.message || ''}
</span>
</div>
<!-- Submit Button -->
<button
type="submit"
disabled="${isSubmitting.value}"
aria-busy="${isSubmitting.value}"
>
${isSubmitting.value ? 'Submitting...' : 'Send Message'}
</button>
</form>
`;
}
};
An accessible accordion with proper ARIA attributes.
const Accordion = {
setup({ signal }) {
const sections = signal([
{ id: "section1", title: "What is Eleva?", content: "Eleva is a minimalist JavaScript framework...", open: true },
{ id: "section2", title: "How do I get started?", content: "Install via npm and import...", open: false },
{ id: "section3", title: "Is it production ready?", content: "Yes, Eleva is stable and tested...", open: false }
]);
const toggleSection = (id) => {
sections.value = sections.value.map(section => ({
...section,
open: section.id === id ? !section.open : section.open
}));
};
return { sections, toggleSection };
},
template({ sections }) {
return `
<div class="accordion" role="tablist" aria-label="FAQ Accordion">
${sections.value.map((section) => `
<div key="${section.id}" class="accordion-item">
<h3>
<button
type="button"
class="accordion-trigger"
id="trigger-${section.id}"
aria-expanded="${section.open}"
aria-controls="panel-${section.id}"
@click="() => toggleSection('${section.id}')"
>
${section.title}
<span class="icon" aria-hidden="true">
${section.open ? '−' : '+'}
</span>
</button>
</h3>
<div
id="panel-${section.id}"
role="region"
aria-labelledby="trigger-${section.id}"
aria-hidden="${!section.open}"
hidden="${!section.open}"
class="accordion-panel"
>
<p>${section.content}</p>
</div>
</div>
`).join('')}
</div>
`;
}
};
A keyboard-navigable tab component.
const TabContainer = {
setup({ signal }) {
const activeTab = signal(0);
const tabs = signal([
{ id: "tab-1", label: "Overview", content: "Overview content goes here..." },
{ id: "tab-2", label: "Features", content: "Features content goes here..." },
{ id: "tab-3", label: "Pricing", content: "Pricing content goes here..." }
]);
const selectTab = (index) => {
activeTab.value = index;
};
const handleKeyDown = (e, index) => {
const tabCount = tabs.value.length;
switch (e.key) {
case 'ArrowLeft':
activeTab.value = (index - 1 + tabCount) % tabCount;
break;
case 'ArrowRight':
activeTab.value = (index + 1) % tabCount;
break;
case 'Home':
activeTab.value = 0;
break;
case 'End':
activeTab.value = tabCount - 1;
break;
}
};
return { activeTab, tabs, selectTab, handleKeyDown };
},
template({ activeTab, tabs }) {
return `
<div class="tab-container">
<!-- Tab List -->
<div role="tablist" aria-label="Content Tabs" class="tab-list">
${tabs.value.map((tab, index) => `
<button
key="${tab.id}"
role="tab"
id="${tab.id}"
aria-selected="${activeTab.value === index}"
aria-controls="panel-${tab.id}"
tabindex="${activeTab.value === index ? 0 : -1}"
class="tab-button ${activeTab.value === index ? 'active' : ''}"
@click="() => selectTab(${index})"
@keydown="(e) => handleKeyDown(e, ${index})"
>
${tab.label}
</button>
`).join('')}
</div>
<!-- Tab Panels -->
${tabs.value.map((tab, index) => `
<div
key="${tab.id}"
role="tabpanel"
id="panel-${tab.id}"
aria-labelledby="${tab.id}"
hidden="${activeTab.value !== index}"
tabindex="0"
class="tab-panel"
>
${tab.content}
</div>
`).join('')}
</div>
`;
}
};
A sortable, selectable data table with proper accessibility.
const DataTable = {
setup({ signal }) {
const sortColumn = signal("name");
const sortDirection = signal("asc");
const selectedRows = signal(new Set());
const data = signal([
{ id: 1, name: "Alice", email: "alice@example.com", role: "Admin" },
{ id: 2, name: "Bob", email: "bob@example.com", role: "User" },
{ id: 3, name: "Charlie", email: "charlie@example.com", role: "User" },
{ id: 4, name: "Diana", email: "diana@example.com", role: "Editor" }
]);
const sortedData = () => {
const sorted = [...data.value].sort((a, b) => {
const aVal = a[sortColumn.value];
const bVal = b[sortColumn.value];
const direction = sortDirection.value === "asc" ? 1 : -1;
return aVal.localeCompare(bVal) * direction;
});
return sorted;
};
const toggleSort = (column) => {
if (sortColumn.value === column) {
sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
} else {
sortColumn.value = column;
sortDirection.value = "asc";
}
};
const toggleRowSelection = (id) => {
const newSelection = new Set(selectedRows.value);
if (newSelection.has(id)) {
newSelection.delete(id);
} else {
newSelection.add(id);
}
selectedRows.value = newSelection;
};
const isSelected = (id) => selectedRows.value.has(id);
return { sortColumn, sortDirection, sortedData, toggleSort, toggleRowSelection, isSelected };
},
template({ sortColumn, sortDirection, sortedData, isSelected }) {
const columns = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "role", label: "Role" }
];
return `
<table class="data-table" role="grid" aria-label="User Data">
<thead>
<tr>
<th scope="col">
<span class="visually-hidden">Select</span>
</th>
${columns.map(col => `
<th
key="${col.key}"
scope="col"
aria-sort="${sortColumn.value === col.key
? (sortDirection.value === 'asc' ? 'ascending' : 'descending')
: 'none'}"
data-column="${col.key}"
>
<button @click="() => toggleSort('${col.key}')">
${col.label}
<span aria-hidden="true">
${sortColumn.value === col.key
? (sortDirection.value === 'asc' ? '▲' : '▼')
: '⇅'}
</span>
</button>
</th>
`).join('')}
</tr>
</thead>
<tbody>
${sortedData().map(row => `
<tr
key="${row.id}"
data-row-id="${row.id}"
aria-selected="${isSelected(row.id)}"
class="${isSelected(row.id) ? 'selected' : ''}"
>
<td>
<input
type="checkbox"
checked="${isSelected(row.id)}"
@change="() => toggleRowSelection(${row.id})"
aria-label="Select ${row.name}"
/>
</td>
<td data-label="Name">${row.name}</td>
<td data-label="Email">${row.email}</td>
<td data-label="Role">${row.role}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
};
An accessible modal dialog with focus management.
const ModalDialog = {
setup({ signal }) {
const isOpen = signal(false);
const modalTitle = signal("Confirm Action");
const modalMessage = signal("Are you sure you want to proceed?");
const openModal = () => {
isOpen.value = true;
// Focus trap would be implemented here
};
const closeModal = () => {
isOpen.value = false;
};
const handleConfirm = () => {
console.log("Confirmed!");
closeModal();
};
const handleKeyDown = (e) => {
if (e.key === "Escape") {
closeModal();
}
};
return { isOpen, modalTitle, modalMessage, openModal, closeModal, handleConfirm, handleKeyDown };
},
template({ isOpen, modalTitle, modalMessage }) {
return `
<div>
<!-- Trigger Button -->
<button
@click="openModal"
aria-haspopup="dialog"
>
Open Modal
</button>
<!-- Modal Backdrop -->
<div
class="modal-backdrop"
hidden="${!isOpen.value}"
aria-hidden="${!isOpen.value}"
@click="closeModal"
></div>
<!-- Modal Dialog -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-hidden="${!isOpen.value}"
hidden="${!isOpen.value}"
class="modal"
@keydown="handleKeyDown"
>
<header class="modal-header">
<h2 id="modal-title">${modalTitle.value}</h2>
<button
@click="closeModal"
aria-label="Close modal"
class="modal-close"
>
×
</button>
</header>
<div id="modal-description" class="modal-body">
<p>${modalMessage.value}</p>
</div>
<footer class="modal-footer">
<button
@click="closeModal"
class="btn-secondary"
>
Cancel
</button>
<button
@click="handleConfirm"
class="btn-primary"
autofocus="${isOpen.value}"
>
Confirm
</button>
</footer>
</div>
</div>
`;
}
};
| ← Back to Features | Next: API Reference → |