Accessibility
Drift provides comprehensive accessibility support for TalkBack (Android) and VoiceOver (iOS).
Overview
Most built-in widgets (Button, Checkbox, Switch, TextField, TabBar) automatically provide semantics. For custom interactive elements, use semantic helper functions.
Semantic Helpers
Tappable
Creates an accessible tappable element:
// Basic tappable
widgets.Tappable("Submit form", submitForm, myButton)
// With custom hint
widgets.TappableWithHint(
"Delete item",
"Double tap to permanently delete",
deleteItem,
deleteIcon,
)
SemanticLabel
Adds an accessibility label to any widget:
widgets.SemanticLabel("User avatar", avatarWidget)
SemanticImage
Marks a widget as an image with a description:
widgets.SemanticImage("Bar chart showing monthly sales", chartWidget)
SemanticHeading
Marks text as a heading for navigation:
// Page title (level 1)
widgets.SemanticHeading(1, widgets.Text{Content: "Welcome"})
// Section heading (level 2)
widgets.SemanticHeading(2, widgets.Text{Content: "Recent Orders"})
SemanticGroup
Groups related widgets into a single accessibility unit:
// Price with currency - announced as "Price: $99.99"
widgets.SemanticGroup(
widgets.Row{
Children: []core.Widget{
widgets.Text{Content: "Price: "},
widgets.Text{Content: "$99.99"},
},
},
)
SemanticLiveRegion
Marks content that updates dynamically:
widgets.SemanticLiveRegion(
widgets.Text{Content: statusMessage},
)
Decorative
Hides purely visual elements from screen readers:
widgets.Decorative(
widgets.Container{Height: 1, Color: colors.Divider},
)
The Semantics Widget
For advanced cases, use the Semantics widget directly:
widgets.Semantics{
Label: "Submit form",
Value: "3 items selected",
Hint: "Double tap to submit",
Role: semantics.SemanticsRoleButton,
Flags: semantics.SemanticsIsEnabled | semantics.SemanticsIsButton,
OnTap: func() { /* handle tap */ },
ChildWidget: myWidget,
}
Semantic Roles
| Role | Use |
|---|---|
SemanticsRoleButton | Clickable button |
SemanticsRoleCheckbox | Checkable item |
SemanticsRoleSwitch | Toggle switch |
SemanticsRoleTextField | Text input |
SemanticsRoleImage | Image content |
SemanticsRoleSlider | Adjustable slider |
SemanticsRoleLink | Hyperlink |
SemanticsRoleHeader | Heading text |
Sliders and Adjustable Controls
// Helper to get pointer to float64
func ptr(v float64) *float64 { return &v }
volume := widgets.Semantics{
Label: "Volume",
Value: fmt.Sprintf("%d%%", currentVolume),
Role: semantics.SemanticsRoleSlider,
CurrentValue: ptr(float64(currentVolume)),
MinValue: ptr(0),
MaxValue: ptr(100),
OnIncrease: func() { setVolume(currentVolume + 10) },
OnDecrease: func() { setVolume(currentVolume - 10) },
ChildWidget: slider,
}
Contrast Validation
import "github.com/go-drift/drift/pkg/validation"
// Check contrast ratio
ratio := validation.ContrastRatio(textColor, backgroundColor)
// Verify WCAG compliance
if validation.MeetsWCAGAA(ratio, false) { // false = normal text
// Meets AA standard (4.5:1)
}
Best Practices
-
Use built-in widgets - Button, Checkbox, Switch have accessibility built-in
-
Use semantic helpers - For custom elements, use
Tappableinstead of rawGestureDetector -
Provide meaningful labels:
// Bad
widgets.Button{Label: "X", OnTap: closeDialog}
// Good
widgets.SemanticLabel("Close dialog",
widgets.Button{Label: "X", OnTap: closeDialog},
) -
Group related content - Use
SemanticGroupfor related elements -
Mark decorative elements - Use
Decorativefor visual-only content -
Use headings - Help screen reader users navigate with
SemanticHeading -
Ensure touch target size - Minimum 48x48 dp for interactive elements
Testing
-
Enable screen reader:
- Android: Settings > Accessibility > TalkBack
- iOS: Settings > Accessibility > VoiceOver
-
Verify all interactive elements are reachable
-
Check that labels are descriptive
-
Run
validation.LintSemanticsTree(root)in tests to check for issues
Next Steps
- API Reference - Accessibility API documentation
- API Reference - Validation API documentation