This guide provides practical examples and best practices for making your Summon applications accessible to everyone, including people with disabilities.
The AccessibleElement component serves as a wrapper for adding accessibility attributes to UI elements:
// Basic usage
AccessibleElement(
role = AccessibilityUtils.NodeRole.BUTTON,
label = "Add to cart",
) {
Box(modifier = Modifier.padding("10px").backgroundColor("#0066cc")) {
Text("Add to cart")
}
}
// With custom role
AccessibleElement(
customRole = "banner",
label = "Site header",
) {
// Header content
}
// With relationships to other elements
AccessibleElement(
role = AccessibilityUtils.NodeRole.TEXTBOX,
label = "Email address",
relations = mapOf(
"describedby" to "email-hint",
"errormessage" to "email-error"
)
) {
// Email input field
}
Use these components to create a properly structured document that conveys meaning to assistive technologies:
Header {
Heading(level = 1) {
Text("My Application")
}
Nav {
// Navigation links
}
}
Main {
Section {
Heading(level = 2) {
Text("Features")
}
Text("Features content")
}
Article {
Heading(level = 2) {
Text("Latest News")
}
Text("News content")
}
}
Aside {
Heading(level = 2) {
Text("Related Information")
}
// Sidebar content
}
Footer {
Text("© 2023 My Company")
}
ARIA (Accessible Rich Internet Applications) attributes provide additional semantics to make web content more accessible:
// Button with accessible label
Button(
onClick = { /* handle click */ },
modifier = Modifier
.ariaLabel("Close dialog")
.ariaControls("main-dialog")
)
// Checkbox with state
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier
.ariaChecked(isChecked)
.ariaDescribedBy("checkbox-hint")
)
// Custom tab implementation
Box(
modifier = Modifier
.role("tab")
.ariaSelected(isSelected)
.tabIndex(if (isSelected) 0 else -1)
)
Proper keyboard focus management is essential for accessibility:
// Make an element focusable but not in tab order
Box(
modifier = Modifier
.focusable()
.onClick { /* handle click */ }
)
// Make an element keyboard tabbable
Box(
modifier = Modifier
.tabbable()
.onClick { /* handle click */ }
)
// Disable an element
Button(
onClick = { /* handle click */ },
enabled = false,
modifier = Modifier.disabled()
)
// Auto-focus an element when it appears
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.autoFocus()
)
TextField and TextArea preserve the native selection while their DOM node is reused. Composition events defer model writes until compositionend, so IME pre-edit text is not overwritten by an intermediate recomposition. Password values are passed through unchanged; trimming or clipboard/reveal policy belongs in the application or approved broker UI.
DatePicker renders a native date input on JS, WASM, and SSR. Its controlled value is an ISO LocalDate, so browser timezone changes cannot shift the chosen calendar day; browser locale and the nearest lang attribute control the native calendar presentation. Supply label for its accessible name and use minDate/maxDate for the announced native range.
Modal renders a named role="dialog" with aria-modal="true", traps Tab and Shift+Tab in the topmost nested dialog, and makes the background inert. Escape and backdrop dismissal are independently controlled:
Modal(
isOpen = showSettings,
onDismiss = { showSettings = false },
ariaLabel = "Account settings",
dismissOnEscape = true,
dismissOnBackdropClick = false,
) {
// Dialog controls
}
Focus returns to the live invoker after dismissal. If that element was removed, focus falls back to the remaining parent dialog without retaining the removed node. Focusable and FocusableContainer report real focus and blur changes; callbacks are detached with their rendered owner.
Buttons have a 44 CSS-pixel minimum target and suppress their transition and hover transform under prefers-reduced-motion: reduce. Toasts expose role="status"/polite announcements, or role="alert"/assertive announcements for errors. ToastManager(maxVisibleToasts) bounds concurrent announcements and evicts the oldest item when the bound is reached.
For mixed-direction content, inherit dir at the component or root and isolate protocol identifiers with Bdi:
Column(Modifier().attribute("dir", "rtl")) {
Text("عنوان الحساب")
Bdi(Modifier().attribute("dir", "ltr")) {
Text("alice@example.test")
}
}
Automated semantic checks do not replace manual keyboard, zoom, high-contrast, screen-reader, OS text-scaling, or physical Safari qualification.
The AccessibilityUtils object provides functions for working with accessibility attributes:
// Create a Modifier with a specific role
val buttonModifier = AccessibilityUtils.createRoleModifier(AccessibilityUtils.NodeRole.BUTTON)
// Create a Modifier with an accessible label
val labelModifier = AccessibilityUtils.createLabelModifier("Close dialog")
// Create a Modifier with relationship to another element
val relationModifier = AccessibilityUtils.createRelationshipModifier("describedby", "description-id")
// Inspect accessibility attributes on a Modifier
val modifier = Modifier()
.role("button")
.ariaLabel("Close")
val accessibilityAttrs = modifier.inspectAccessibility()
// Result: {"role": "button", "aria-label": "Close"}
// Visible focus indicator
Button(
onClick = { /* handle click */ },
modifier = Modifier
.tabbable()
.backgroundColor("#ffffff")
.focusStyle(
Modifier.border("2px", "solid", "#0066cc")
)
)
// Accessible image with alternative text
Image(
src = "/assets/logo.png",
alt = "Company Logo",
modifier = Modifier.ariaHidden(false)
)
// Hidden decorative image
Image(
src = "decoration.png",
alt = "",
modifier = Modifier.ariaHidden(true)
)
// Error state with both color and icon
if (hasError) {
Box(
modifier = Modifier
.color("#d32f2f") // Ensure this has proper contrast
.ariaDescribedBy("error-message")
) {
Icon("error")
Text("This field is required", id = "error-message")
}
}
// Accessible form field with label and validation
FormField {
Label(for = "email-input") {
Text("Email address")
if (isRequired) {
Text("*", modifier = Modifier.ariaHidden(true))
Text(" (required)", modifier = Modifier.className("visually-hidden"))
}
}
TextField(
value = email,
onValueChange = { email = it },
modifier = Modifier
.id("email-input")
.ariaRequired(isRequired)
.ariaInvalid(hasError)
.ariaDescribedBy(if (hasError) "email-error" else "email-hint")
)
if (hasError) {
Text(
"Please enter a valid email address",
id = "email-error",
modifier = Modifier
.color("#d32f2f")
.ariaLive("assertive")
)
} else {
Text(
"Enter your email address in format: name@example.com",
id = "email-hint",
modifier = Modifier.color("#666666")
)
}
}
Keyboard Testing:
Screen Reader Testing:
Automated Testing:
Manual Testing: