ARIA Implementation Examples for Solid-UI
Date: January 15, 2026
Related: Accessibility Checklist, Theme System Analysis
Overview
This document provides complete ARIA implementation examples for the Solid-UI theme system and common components. Copy and adapt these patterns for accessibility-compliant widgets.
1. Theme Switcher Component
Basic Select Implementation
/**
* Creates an accessible theme switcher select element
* @param {Document} dom - DOM context
* @param {Object} options - Configuration options
* @returns {HTMLElement} Theme switcher widget
*/
export function createThemeSwitcher(dom, options = {}) {
const container = dom.createElement('div')
container.className = 'theme-switcher-container'
// Label
const label = dom.createElement('label')
label.htmlFor = 'solid-ui-theme-select'
label.textContent = options.label || 'Color Theme'
// Select element with ARIA
const select = dom.createElement('select')
select.id = 'solid-ui-theme-select'
select.className = 'theme-switcher'
select.setAttribute('aria-label', 'Choose application color theme')
select.setAttribute('aria-describedby', 'theme-help')
// Add themes as options
Object.entries(themeLoader.themes).forEach(([name, path]) => {
const option = dom.createElement('option')
option.value = name
option.textContent = name.charAt(0).toUpperCase() + name.slice(1)
if (name === themeLoader.currentTheme) {
option.selected = true
}
select.appendChild(option)
})
// Help text (visible)
const helpText = dom.createElement('span')
helpText.id = 'theme-help'
helpText.className = 'help-text'
helpText.textContent = 'Changes the color scheme of the application'
// Change handler with announcement
select.addEventListener('change', async (e) => {
const themeName = e.target.value
const themeLabel = e.target.options[e.target.selectedIndex].textContent
// Update theme
await themeLoader.loadTheme(themeName)
// Announce change to screen readers
announceThemeChange(dom, themeLabel)
})
// Assemble
container.appendChild(label)
container.appendChild(select)
container.appendChild(helpText)
return container
}
/**
* Announces theme changes to screen readers
* @param {Document} dom - DOM context
* @param {string} themeName - Name of the theme
*/
function announceThemeChange(dom, themeName) {
// Create or reuse announcement region
let announcement = dom.getElementById('theme-announcement')
if (!announcement) {
announcement = dom.createElement('div')
announcement.id = 'theme-announcement'
announcement.setAttribute('role', 'status')
announcement.setAttribute('aria-live', 'polite')
announcement.setAttribute('aria-atomic', 'true')
announcement.className = 'sr-only'
dom.body.appendChild(announcement)
}
// Update announcement
announcement.textContent = `Theme changed to ${themeName}`
// Clear after announcement (optional)
setTimeout(() => {
announcement.textContent = ''
}, 1000)
}
Advanced Custom Dropdown Implementation
/**
* Creates an accessible custom theme switcher with dropdown
* Follows ARIA combobox pattern
*/
export function createCustomThemeSwitcher(dom, options = {}) {
const container = dom.createElement('div')
container.className = 'custom-theme-switcher'
// Button trigger
const button = dom.createElement('button')
button.type = 'button'
button.id = 'theme-button'
button.className = 'theme-button'
button.setAttribute('aria-haspopup', 'listbox')
button.setAttribute('aria-expanded', 'false')
button.setAttribute('aria-labelledby', 'theme-button-label')
const buttonLabel = dom.createElement('span')
buttonLabel.id = 'theme-button-label'
buttonLabel.textContent = 'Choose Theme: '
const currentTheme = dom.createElement('span')
currentTheme.textContent = themeLoader.currentTheme
currentTheme.setAttribute('aria-live', 'polite')
button.appendChild(buttonLabel)
button.appendChild(currentTheme)
// Listbox for options
const listbox = dom.createElement('ul')
listbox.id = 'theme-listbox'
listbox.className = 'theme-listbox'
listbox.setAttribute('role', 'listbox')
listbox.setAttribute('aria-labelledby', 'theme-button-label')
listbox.style.display = 'none'
let selectedIndex = 0
const themes = Object.entries(themeLoader.themes)
themes.forEach(([name, path], index) => {
const option = dom.createElement('li')
option.setAttribute('role', 'option')
option.id = `theme-option-${name}`
option.textContent = name.charAt(0).toUpperCase() + name.slice(1)
option.dataset.value = name
if (name === themeLoader.currentTheme) {
option.setAttribute('aria-selected', 'true')
option.className = 'theme-option selected'
selectedIndex = index
} else {
option.setAttribute('aria-selected', 'false')
option.className = 'theme-option'
}
// Click handler
option.addEventListener('click', () => {
selectTheme(name, option)
})
listbox.appendChild(option)
})
// Toggle dropdown
function toggleDropdown(show) {
const isExpanded = show !== undefined ? show : button.getAttribute('aria-expanded') === 'false'
button.setAttribute('aria-expanded', String(isExpanded))
listbox.style.display = isExpanded ? 'block' : 'none'
if (isExpanded) {
// Focus first or selected option
const selectedOption = listbox.querySelector('[aria-selected="true"]')
if (selectedOption) {
selectedOption.focus()
}
}
}
// Select theme
function selectTheme(themeName, option) {
// Update UI
listbox.querySelectorAll('[role="option"]').forEach(opt => {
opt.setAttribute('aria-selected', 'false')
opt.classList.remove('selected')
})
option.setAttribute('aria-selected', 'true')
option.classList.add('selected')
currentTheme.textContent = option.textContent
// Load theme
themeLoader.loadTheme(themeName)
// Close dropdown
toggleDropdown(false)
button.focus()
// Announce
announceThemeChange(dom, option.textContent)
}
// Keyboard navigation
listbox.addEventListener('keydown', (e) => {
const options = Array.from(listbox.querySelectorAll('[role="option"]'))
const currentIndex = options.findIndex(opt => opt === dom.activeElement)
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
if (currentIndex < options.length - 1) {
options[currentIndex + 1].focus()
}
break
case 'ArrowUp':
e.preventDefault()
if (currentIndex > 0) {
options[currentIndex - 1].focus()
}
break
case 'Home':
e.preventDefault()
options[0].focus()
break
case 'End':
e.preventDefault()
options[options.length - 1].focus()
break
case 'Enter':
case ' ':
e.preventDefault()
if (dom.activeElement.hasAttribute('data-value')) {
selectTheme(dom.activeElement.dataset.value, dom.activeElement)
}
break
case 'Escape':
e.preventDefault()
toggleDropdown(false)
button.focus()
break
}
})
// Button click
button.addEventListener('click', () => {
toggleDropdown()
})
// Click outside to close
dom.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
toggleDropdown(false)
}
})
container.appendChild(button)
container.appendChild(listbox)
return container
}
2. Button Components
Standard Button with Icon
/**
* Creates an accessible button with icon
*/
export function createIconButton(dom, options) {
const button = dom.createElement('button')
button.type = options.type || 'button'
button.className = options.className || 'icon-button'
// Accessible label (required for icon-only buttons)
button.setAttribute('aria-label', options.label)
// Optional tooltip
if (options.tooltip) {
button.setAttribute('title', options.tooltip)
}
// Icon (decorative, hidden from screen readers)
const icon = dom.createElement('span')
icon.className = 'icon'
icon.setAttribute('aria-hidden', 'true')
icon.textContent = options.icon || '⚙️'
button.appendChild(icon)
// Click handler
if (options.onClick) {
button.addEventListener('click', options.onClick)
}
return button
}
// Usage
const settingsButton = createIconButton(dom, {
label: 'Open settings',
tooltip: 'Settings',
icon: '⚙️',
onClick: () => openSettings()
})
Toggle Button (Switch)
/**
* Creates an accessible toggle/switch button
*/
export function createToggleButton(dom, options) {
const button = dom.createElement('button')
button.type = 'button'
button.className = 'toggle-button'
button.setAttribute('role', 'switch')
button.setAttribute('aria-checked', String(options.checked || false))
button.setAttribute('aria-label', options.label)
// Visual indicator
const indicator = dom.createElement('span')
indicator.className = 'toggle-indicator'
indicator.setAttribute('aria-hidden', 'true')
const labelSpan = dom.createElement('span')
labelSpan.textContent = options.label
button.appendChild(indicator)
button.appendChild(labelSpan)
// Toggle handler
button.addEventListener('click', () => {
const isChecked = button.getAttribute('aria-checked') === 'true'
button.setAttribute('aria-checked', String(!isChecked))
if (options.onChange) {
options.onChange(!isChecked)
}
})
return button
}
// Usage
const darkModeToggle = createToggleButton(dom, {
label: 'Dark mode',
checked: false,
onChange: (enabled) => {
if (enabled) {
themeLoader.loadTheme('dark')
} else {
themeLoader.loadTheme('light')
}
}
})
Button with Loading State
/**
* Creates a button that can show loading state
*/
export function createLoadingButton(dom, options) {
const button = dom.createElement('button')
button.type = options.type || 'button'
button.className = 'loading-button'
const text = dom.createElement('span')
text.className = 'button-text'
text.textContent = options.text
const spinner = dom.createElement('span')
spinner.className = 'button-spinner'
spinner.setAttribute('aria-hidden', 'true')
spinner.textContent = '⏳'
spinner.style.display = 'none'
button.appendChild(text)
button.appendChild(spinner)
// Set loading state
button.setLoading = (isLoading) => {
button.disabled = isLoading
button.setAttribute('aria-busy', String(isLoading))
if (isLoading) {
text.textContent = options.loadingText || 'Loading...'
spinner.style.display = 'inline-block'
} else {
text.textContent = options.text
spinner.style.display = 'none'
}
}
// Click handler
if (options.onClick) {
button.addEventListener('click', async () => {
button.setLoading(true)
try {
await options.onClick()
} finally {
button.setLoading(false)
}
})
}
return button
}
// Usage
const saveButton = createLoadingButton(dom, {
text: 'Save Changes',
loadingText: 'Saving...',
onClick: async () => {
await saveData()
}
})
3. Form Components
Accessible Text Input
/**
* Creates an accessible text input field
*/
export function createTextInput(dom, options) {
const container = dom.createElement('div')
container.className = 'form-field'
// Label (required)
const label = dom.createElement('label')
label.htmlFor = options.id
label.textContent = options.label
if (options.required) {
label.innerHTML += ' <span aria-label="required">*</span>'
}
// Input
const input = dom.createElement('input')
input.type = options.type || 'text'
input.id = options.id
input.name = options.name || options.id
input.className = 'form-input'
if (options.required) {
input.setAttribute('aria-required', 'true')
input.required = true
}
if (options.placeholder) {
input.placeholder = options.placeholder
}
// Help text
const helpId = `${options.id}-help`
const helpText = dom.createElement('span')
helpText.id = helpId
helpText.className = 'help-text'
helpText.textContent = options.helpText || ''
// Error text
const errorId = `${options.id}-error`
const errorText = dom.createElement('span')
errorText.id = errorId
errorText.className = 'error-text'
errorText.setAttribute('role', 'alert')
errorText.style.display = 'none'
// Link descriptions
const describedBy = [helpId]
if (options.showError) {
describedBy.push(errorId)
}
input.setAttribute('aria-describedby', describedBy.join(' '))
// Validation
input.setError = (errorMessage) => {
if (errorMessage) {
input.setAttribute('aria-invalid', 'true')
input.classList.add('error')
errorText.textContent = errorMessage
errorText.style.display = 'block'
} else {
input.setAttribute('aria-invalid', 'false')
input.classList.remove('error')
errorText.textContent = ''
errorText.style.display = 'none'
}
}
// Assemble
container.appendChild(label)
container.appendChild(input)
if (options.helpText) {
container.appendChild(helpText)
}
container.appendChild(errorText)
return { container, input }
}
// Usage
const { container, input } = createTextInput(dom, {
id: 'username',
label: 'Username',
required: true,
helpText: 'Must be 3-20 characters',
placeholder: 'Enter username'
})
// Validation example
input.addEventListener('blur', () => {
if (input.value.length < 3) {
input.setError('Username must be at least 3 characters')
} else {
input.setError(null)
}
})
4. Chat Components
Chat Message with ARIA
/**
* Creates an accessible chat message
*/
export function createChatMessage(dom, options) {
const message = dom.createElement('li')
message.className = 'chat-message'
message.setAttribute('role', 'article')
// Author
const author = dom.createElement('span')
author.className = 'message-author'
author.textContent = options.author
// Timestamp
const timestamp = dom.createElement('time')
timestamp.className = 'message-time'
timestamp.setAttribute('datetime', options.timestamp)
timestamp.textContent = formatTime(options.timestamp)
// Content
const content = dom.createElement('p')
content.className = 'message-content'
content.textContent = options.content
message.appendChild(author)
message.appendChild(timestamp)
message.appendChild(content)
return message
}
### Chat Container with Live Region
```javascript
/**
* Creates an accessible chat container
*/
export function createChatContainer(dom, options) {
const container = dom.createElement('div')
container.className = 'chat-container'
container.setAttribute('role', 'log')
container.setAttribute('aria-label', options.label || 'Chat messages')
// Message list
const messageList = dom.createElement('ul')
messageList.className = 'message-list'
messageList.setAttribute('aria-live', 'polite')
messageList.setAttribute('aria-atomic', 'false')
messageList.setAttribute('aria-relevant', 'additions')
// Screen reader announcement region (separate from visual)
const srAnnouncement = dom.createElement('div')
srAnnouncement.className = 'sr-only'
srAnnouncement.setAttribute('role', 'status')
srAnnouncement.setAttribute('aria-live', 'polite')
srAnnouncement.setAttribute('aria-atomic', 'true')
// Add message function
container.addMessage = (messageData) => {
// Add to visual list
const message = createChatMessage(dom, messageData)
messageList.appendChild(message)
// Announce to screen readers
srAnnouncement.textContent = `New message from ${messageData.author}: ${messageData.content}`
// Clear announcement after it's been read
setTimeout(() => {
srAnnouncement.textContent = ''
}, 1000)
// Scroll to bottom
messageList.scrollTop = messageList.scrollHeight
}
container.appendChild(messageList)
container.appendChild(srAnnouncement)
return container
}
5. Dialog/Modal Components
/**
* Creates an accessible modal dialog
*/
export function createDialog(dom, options) {
// Backdrop
const backdrop = dom.createElement('div')
backdrop.className = 'dialog-backdrop'
backdrop.setAttribute('aria-hidden', 'true')
// Dialog container
const dialog = dom.createElement('div')
dialog.className = 'dialog'
dialog.setAttribute('role', 'dialog')
dialog.setAttribute('aria-modal', 'true')
dialog.setAttribute('aria-labelledby', 'dialog-title')
dialog.setAttribute('aria-describedby', 'dialog-description')
// Title
const title = dom.createElement('h2')
title.id = 'dialog-title'
title.textContent = options.title
// Description
const description = dom.createElement('p')
description.id = 'dialog-description'
description.textContent = options.description
// Content
const content = dom.createElement('div')
content.className = 'dialog-content'
if (options.content) {
content.appendChild(options.content)
}
// Actions
const actions = dom.createElement('div')
actions.className = 'dialog-actions'
const cancelButton = dom.createElement('button')
cancelButton.type = 'button'
cancelButton.textContent = options.cancelText || 'Cancel'
cancelButton.addEventListener('click', () => dialog.close())
const confirmButton = dom.createElement('button')
confirmButton.type = 'button'
confirmButton.textContent = options.confirmText || 'Confirm'
confirmButton.addEventListener('click', () => {
if (options.onConfirm) {
options.onConfirm()
}
dialog.close()
})
actions.appendChild(cancelButton)
actions.appendChild(confirmButton)
// Assemble
dialog.appendChild(title)
dialog.appendChild(description)
dialog.appendChild(content)
dialog.appendChild(actions)
backdrop.appendChild(dialog)
// Focus trap
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
let firstFocusable, lastFocusable
function updateFocusableElements() {
const focusables = dialog.querySelectorAll(focusableElements)
firstFocusable = focusables[0]
lastFocusable = focusables[focusables.length - 1]
}
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
dialog.close()
return
}
if (e.key === 'Tab') {
if (e.shiftKey) {
if (dom.activeElement === firstFocusable) {
e.preventDefault()
lastFocusable.focus()
}
} else {
if (dom.activeElement === lastFocusable) {
e.preventDefault()
firstFocusable.focus()
}
}
}
})
// Open/close methods
let previousFocus
dialog.open = () => {
previousFocus = dom.activeElement
dom.body.appendChild(backdrop)
updateFocusableElements()
firstFocusable.focus()
dom.body.style.overflow = 'hidden'
}
dialog.close = () => {
backdrop.remove()
dom.body.style.overflow = ''
if (previousFocus) {
previousFocus.focus()
}
}
// Close on backdrop click
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
dialog.close()
}
})
return dialog
}
// Usage
const confirmDialog = createDialog(dom, {
title: 'Confirm Action',
description: 'Are you sure you want to delete this item?',
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: () => {
deleteItem()
}
})
confirmDialog.open()
6. Utility Functions
Focus Management
/**
* Manages focus for dynamic content
*/
export const focusManager = {
/**
* Stores current focus element
*/
storeFocus() {
this._previousFocus = document.activeElement
},
/**
* Restores previously focused element
*/
restoreFocus() {
if (this._previousFocus && this._previousFocus.focus) {
this._previousFocus.focus()
}
},
/**
* Moves focus to element
*/
moveFocusTo(element) {
if (element && element.focus) {
element.focus()
}
},
/**
* Gets all focusable elements in container
*/
getFocusableElements(container) {
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
return Array.from(container.querySelectorAll(selector))
.filter(el => !el.disabled && el.offsetParent !== null)
}
}
Announcement Helpers
/**
* Announces message to screen readers
*/
export function announce(dom, message, priority = 'polite') {
const announcement = dom.createElement('div')
announcement.setAttribute('role', priority === 'assertive' ? 'alert' : 'status')
announcement.setAttribute('aria-live', priority)
announcement.setAttribute('aria-atomic', 'true')
announcement.className = 'sr-only'
announcement.textContent = message
dom.body.appendChild(announcement)
setTimeout(() => {
announcement.remove()
}, 1000)
}
// Usage
announce(dom, 'Theme changed successfully')
announce(dom, 'Error: Could not save changes', 'assertive')
Complete CSS for Accessibility
/* accessibility.css - Core accessibility styles */
/* Screen reader only content */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
overflow: visible;
clip: auto;
white-space: normal;
}
/* Focus indicators */
:focus-visible {
outline: 2px solid var(--sui-focus-color, #667eea);
outline-offset: 2px;
}
/* Skip links */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--sui-primary);
color: white;
padding: 0.5em 1em;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
/* High contrast mode */
@media (prefers-contrast: high) {
:root {
--sui-border: #000;
--sui-text: #000;
--sui-bg: #fff;
}
button,
input,
select {
border: 2px solid currentColor;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Touch target size */
button,
a,
input,
select,
textarea {
min-height: 44px;
min-width: 44px;
}
/* Visible focus for keyboard users only */
:focus:not(:focus-visible) {
outline: none;
}
Status: Reference Implementation
Last Updated: January 15, 2026
Maintainer: Solid-UI Team
ARIA Implementation Examples for Solid-UI
Date: January 15, 2026
Related: Accessibility Checklist, Theme System Analysis
Overview
This document provides complete ARIA implementation examples for the Solid-UI theme system and common components. Copy and adapt these patterns for accessibility-compliant widgets.
1. Theme Switcher Component
Basic Select Implementation
Advanced Custom Dropdown Implementation
2. Button Components
Standard Button with Icon
Toggle Button (Switch)
Button with Loading State
3. Form Components
Accessible Text Input
4. Chat Components
Chat Message with ARIA
5. Dialog/Modal Components
6. Utility Functions
Focus Management
Announcement Helpers
Complete CSS for Accessibility
Status: Reference Implementation
Last Updated: January 15, 2026
Maintainer: Solid-UI Team