Skip to main content


Making truly accessible apps is complex and requires familiarity with a wide range of concepts. The Web Content Accessibility Guidelines provide recommendations for web apps.

Module Required

You must include the AccessibilityModule in your application in order to use these features.

package accessibility.module import io.nacular.doodle.application.Modules.Companion.AccessibilityModule import io.nacular.doodle.application.application import org.kodein.di.instance import rendering.MyApp fun main() { //sampleStart application(modules = listOf(AccessibilityModule)) { MyApp(instance()) } //sampleEnd }

Doodle uses opt-in modules like this to improve bundle size.


Only Web apps support this feature currently, as Desktop is still in alpha.

package accessibility import io.nacular.doodle.application.Application import io.nacular.doodle.application.Modules.Companion.AccessibilityModule import io.nacular.doodle.application.application //sampleStart class MyApp(/*...*/): Application { init { // use accessibility features } override fun shutdown() {} } fun main () { // Include the AccessibilityModule to enable features application(modules = listOf(AccessibilityModule)) { MyApp(/*...*/) } } //sampleEnd

Descriptive Text


Authors can provide short, descriptive text that is used by assistive technologies to announce a View when it is selected. This property helps in cases where a View contains no meaningful text.

package accessibility import io.nacular.doodle.controls.buttons.PushButton //sampleStart val button = PushButton("x").apply { accessibilityLabel = "Close the window" } //sampleEnd


In many cases the app presents descriptive text to the user directly using other Views, like labels for a text fields. The accessibilityLabelProvider points to another View that should be used as a "label" for the current one.

package accessibility import io.nacular.doodle.controls.text.Label import io.nacular.doodle.controls.text.TextField //sampleStart val label = Label("Enter your name") val textField = TextField().apply { accessibilityLabelProvider = label } //sampleEnd

Views can be linked this way at any time, even if they are not both currently displayed. Doodle will track the relationship, and surface it to assistive technologies if the Views are simultaneously displayed.


Labels should be short descriptive names for a View. But it is possible to provide more detailed descriptions as well via the accessibilityDescriptionProvider. This property behaves like accessibilityLabelProvider, but is intended for longer, more detailed text that describes the View.

Widget Roles

Authors can indicate that a View plays a well-defined role as a widget by tagging it with an accessibility role. This enables assistive technologies to change the presentation of the View to the user as she navigates a scene. This is done by setting the View's accessibilityRole.

Here is an example of creating a View that will serve as a ButtonRole.

package accessibility import io.nacular.doodle.accessibility.ButtonRole import io.nacular.doodle.core.View //sampleStart class CustomButton: View(accessibilityRole = ButtonRole()) { // ... } //sampleEnd

This View will now be treated as a button by accessibility technologies (i.e. screen readers). The button role itself does not have additional properties, so simply adopting it is sufficient.

Other roles have state and must be synchronized with the View to ensure proper assistive support.

package accessibility import io.nacular.doodle.accessibility.ToggleButtonRole import io.nacular.doodle.core.View import io.nacular.doodle.utils.observable //sampleStart class CustomToggle(private val role: ToggleButtonRole = ToggleButtonRole()): View(accessibilityRole = role) { var selected by observable(false) { old, new -> role.pressed = new } } //sampleEnd

Many of the widgets in the Controls library ship with accessibility support (though this continues to improve). The library also provides bindings for some roles and models, which makes it easier to synchronize roles with their widgets. ValueSlider, for example, binds its role to the ConfinedValueModel that underlies it. This way the role and View are always in sync.