An Accessible App. Where to start?
Making your app accessible to everyone: where do you start? Below are the main steps that will help you make your app usable for the widest possible group of people, including people with disabilities.

It can seem very complicated and even intimidating when you delve into the vast amount of information on digital accessibility. How do you incorporate all those guidelines, insights and code examples into an app? To get you started, we have listed the six most important steps for you.
Use a screen reader!
Offer textual alternatives.
Make it clear what the purpose is.
Make sure text can be resized.
Provide sufficient contrast.
Show focus and offer a logical order.
Below is a detailed explanation of each step. Finally, we explain how you can test whether it works as intended.
Note that this list is not exhaustive and will not make your app fully accessible. But is meant to make the biggest possible difference through a few simple steps.
1. Use a screen reader!
To properly test whether your app is accessible, it pays to learn how to use a screen reader.
A screen reader is mainly used by visually impaired people. However, by optimising your app for this, you ensure that a much larger group of people can use your app. Users of voice, keyboard and switch controls will also benefit, and you automatically take into account a lot of success criteria from the WCAG.
This makes the screen reader the ideal tool to test for accessibility. But it can take some getting used to. It all works a little differently from what you are used to. With the ScreenReader app, you learn all the gestures and possibilities as you go.
2. Offer textual alternatives
Do all buttons have a clear label? Do icons, images, graphics, among others, have a textual alternative?
Make sure alternative text is available for all content without text. These include images, icons and graphs. Describe what can be seen. People who are blind have this description read aloud through their screen reader. Alternative text can also be useful for anyone who is unsure about the meaning of the content.
You do this by setting an accessibility label. You can see how to do this for your app in the example below.
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility label - Android
On Android, you can use the contentDescription
attribute to set an accessibility label.
You can also pass any kind of Span
for greater control over pronunciation. For example, you can set a language by using LocaleSpan
.
If another element is used to display the label, you can link the label by using the labelFor
attribute.
// Set accessibility label
element.contentDescription = "Appt"
// Set accessibility label in Dutch language
val locale = Locale.forLanguageTag("nl-NL")
val localeSpan = LocaleSpan(locale)
val string = SpannableString("Appt")
string.setSpan(localeSpan, 0, string.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
element.contentDescription = localeSpan
// Link visual label to field
textView.setLabelFor(R.id.editText)
Accessibility label - Jetpack Compose
In Jetpack Compose, you can use the contentDescription
or text
properties to set an accessibility label.
ContentDescription
is used for more visual elements, like icons and images. Text
is used for text elements.
You can pass any kind of AnnotatedString
for greater control over pronunciation.
// set contentDescription
Box(modifier = Modifier.semantics {
contentDescription = "Appt"
}) {
// Box content...
}
// set text
Box(modifier = Modifier.semantics {
text = AnnotatedString("Appt")
}) {
// Box content...
}
Accessibility label - iOS
On iOS, you can use the accessibilityLabel
property to set an accessibility label.
You can also use the attributedAccessibilityLabel
property for greater control over pronunciation. For example, spell out each character with .accessibilitySpeechPunctuation
or set a language using .accessibilitySpeechLanguage
.
The accessibility label should be as short as possible, while still being intuitive. When long labels cannot be avoided, you should use accessibilityUserInputLabels
to provide alternative labels. The primary label is first in the array, optionally followed by alternative labels in descending order of importance.
If another element is used to display the label, you can link the label by setting isAccessibilityElement
to false
and setting accessibilityLabel
to the value
of the label.
// Set accessibility label
element.accessibilityLabel = "Appt"
// Set accessibility label with Dutch speech engine
element.attributedAccessibilityLabel = NSAttributedString(
string: "Appt",
attributes: [.accessibilitySpeechLanguage: "nl-NL"]
)
// Set accessibility label for controls
element.accessibilityUserInputLabels = ["Appt", "Alternative"]
// Link visual label
label.isAccessibilityElement = false
element.accessibilityLabel = label.text
Accessibility label - SwiftUI
In SwiftUI, the accessibilityLabel(_:)
view modifier is used to assign an accessibility label to a view. It's essential to keep the accessibility label concise yet meaningful for optimal usability.
Image("appt_logo")
.accessibilityLabel("Appt logo")
You can enhance accessibility in SwiftUI by using the Text
initializer with an AttributedString
parameter, offering precise control over pronunciation. For instance, you can spell out each character using .accessibilitySpeechPunctuation
or specify a language with .accessibilitySpeechLanguage
.
let attributeContainer = AttributeContainer([
.accessibilitySpeechPunctuation: true,
.accessibilitySpeechLanguage: "en_US"
])
// Initialize `AttributedString` with the desired text and the `AttributeContainer`
let attributedString = AttributedString("Appt", attributes: attributeContainer)
Text(attributedString)
When dealing with long labels, consider using accessibilityInputLabels
to provide alternative labels. The primary label comes first in the array, followed by optional alternative labels arranged in decreasing order of relevance.
Link(destination: URL(string: "https://appt.org/en/")!, label: {
Text("Appt.org")
})
// Primary and alternative labels in descending order of importance
.accessibilityInputLabels([
"Appt website",
"Appt"
])
Accessibility label - Flutter
In Flutter, the semanticsLabel
property is used as accessibility name.
You can also use the attributedLabel
property for greater control over pronunciation. For example, spell out each character with SpellOutStringAttribute
or set a language using LocaleStringAttribute
.
For even more control, you can use the Semantics
widget. For example, if you want to ignore the semantics of underlaying widgets, you can set the excludeSemantics
attribute to true
.
Control(
semanticsLabel: 'Appt'
)
Semantics(
label: 'Appt',
attributedLabel: AttributedString('Appt', attributes: [
SpellOutStringAttribute(range: const TextRange(start: 0, end: 3))
]),
excludeSemantics: true;
);
Accessibility label - React Native
In React Native, you can set an accessibility label by using the accessibilityLabel
prop.
<Control
accessibilityLabel="Appt" />
Accessibility label - .NET MAUI
In MAUI, you can set an accessibility label by using the SemanticProperties.Description
property.
<Control
SemanticProperties.Description="Appt" />
As an alternative, you can link a label by setting IsInAccessibleTree
to false
and setting SemanticProperties.Description
the value
of the label.
<Image
Source="appt.png"
SemanticProperties.Description="{Binding Source={x:Reference Welcome}, Path=Text}"
HeightRequest="200"
HorizontalOptions="Center" />
<Label
x:Name="Welcome"
AutomationProperties.IsInAccessibleTree="False"
Text="Welcome to Appt"
FontSize="18"
HorizontalOptions="Center" />
Note: SemanticProperties.Description
will supersede the value of AutomationProperties.IsInAccessibleTree
.
In the sample below, the text from SemanticProperties.Description
will be spoken regardless of the value of AutomationProperties.IsInAccessibleTree
.
<Label
x:Name="Welcome"
AutomationProperties.IsInAccessibleTree="False"
SemanticProperties.Description="Welcome to Appt (will be sproken)"
Text="Welcome to Appt"
FontSize="18"
HorizontalOptions="Center" />
Accessibility label - Xamarin
In Xamarin Forms, you can set an accessibility label by using the AutomationProperties.Name
property.
If another element is used to display the label, the AutomationProperties.LabeledBy
property be used to link a label. Unfortunately, LabeledBy
only works on Android.
As an alternative, you can link a label by setting IsInAccessibleTree
to false
and setting AutomationProperties.Name
the value
of the label.
<Control
AutomationProperties.Name="Appt" />
3. Make clear what the purpose is
Is it clear what action can be performed?
Set a name. The name is used for identification. Setting a name allows tools such as voice control to perform targeted actions.
Set accessibility name
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility name - Android
On Android, the contentDescription
property is used as accessibility name.
element.contentDescription = "Appt"
Accessibility name - Jetpack Compose
In Jetpack Compose, the contentDescription
attribute is used to set an accessibility name.
Box(modifier = Modifier.semantics {
contentDescription = "Appt"
}) {
// Box content...
}
Accessibility name - iOS
On iOS, accessibilityLabel
property is used as accessibility name.
element.accessibilityLabel = "Appt"
Accessibility name - SwiftUI
In SwiftUI, the accessibilityLabel
property is used as accessibility name.
Button {
// Button action
} label: {
Image(systemName: "magnifyingglass")
// Set accessibility label
.accessibilityLabel("Search")
}
Accessibility name - Flutter
In Flutter, the semanticsLabel
property is used as accessibility name.
Control(
semanticsLabel: 'Appt'
);
Accessibility name - React Native
In React Native, the accessibilityLabel
prop is used accessibility name.
<Control
accessibilityLabel="Appt" />
Accessibility name - .NET MAUI
In MAUI, the SemanticProperties.Description
property is used as the accessibility name.
<Control
SemanticProperties.Description="Appt" />
Warning:
Avoid setting the
Description
attached property on aLabel
. This will prevent theText
property from being spoken by the screen reader. The visual text should ideally match the text read aloud by the screen reader.Avoid setting the
Description
attached property on anEntry
orEditor
on Android. Doing so will stop TalkBack actions from functioning. Instead, use thePlaceholder
property or theHint
attached property.On iOS, if you set the
Description
property on any control that has children, the screen reader will be unable to reach the children. This is because iOS doesn't provide accessibility features that allow navigation from a parent element into a child element.
Accessibility name - Xamarin
In Xamarin, the AutomationProperties.Name
property is used as accessibility name.
<Control
AutomationProperties.Name="Appt" />
Set a role. With the role "button", it is clear that an action takes place on activation. With the role "link", it is clear that you are directed to another location. Setting a role makes it clear to tool users what they can do.
Set accessibility role
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility role - Android
On Android, you can use the setAccessibilityDelegate
method of ViewCompat
to get a reference to AccessibilityNodeInfoCompat
. This object contains many useful accessibility related methods.
You can set a role using the setRoleDescription
method. However, we recommend using the setClassName
method over setRoleDescription
to support multilingual roles. For example, set Button::class.java.name
if an element behaves like a button. The role will be set to Button
in English, and to its respective translation in other languages.
Element type | Class name |
---|---|
Button | android.widget.Button |
Checkbox | android.widget.CompoundButton |
Drop down list | android.widget.Spinner |
Edit box | android.widget.EditText |
Image | android.widget.ImageView |
Toggle button | android.widget.ToggleButton |
Radio button | android.widget.RadioButton |
Progress bar | android.widget.ProgressBar |
Value picker | android.widget.NumberPicker |
To indicate other element types, such as Switch
or Tab
, use setRoleDescription
.
You can indicate a heading by using the setHeading
method. ViewCompat
also contains a convenience method: setAccessibilityHeading
.
ViewCompat.setAccessibilityDelegate(
element,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
// Button
info.className = Button::class.java.name
// Image
info.className = ImageView::class.java.name
// Heading
info.isHeading = true
// Custom
info.roleDescription = "Custom role"
}
}
)
// Convenience method
ViewCompat.setAccessibilityHeading(view, true)
EditText considerations
Hidden in TalkBack's source code there's a special case for EditText
:
if (ClassLoadingCache.checkInstanceOf(className, android.widget.EditText.class)) {
if (node.isEnabled() && !node.isEditable()) {
// Developers may want to provide extra information
// when an EditText is enabled but not editable.
return ROLE_NONE;
} else {
return ROLE_EDIT_TEXT;
}
}
Meaning that in order for the correct role to be announced, the EditText
needs to be editable, otherwise ROLE_NONE
will be set to the component.
ViewCompat.setAccessibilityDelegate(
element,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
// EditText
info.className = EditText::class.java.name
info.isEditable = true
}
}
)
// Convenience method
ViewCompat.setAccessibilityHeading(view, true)
Accessibility role - Jetpack Compose
In Jetpack Compose you can use role
to set the role of an element.
The following constants are defined for the Role
:
Button
: this element is a buttonCheckbox
: this element is checkbox with two states (checked / unchecked)DropdownList
: this element is drop down menuImage
: this element is an imageRadioButton
: this element is a radio buttonSwitch
: this element is a switchTab
: this element is a tab which represents a single page of content using a text label and/or iconValuePicker
: this element is a value picker and should support accessibility scroll events
For more information about the mapping, view the Role
class, Role.toLegacyClassName()
method and populateAccessibilityNodeInfoProperties()
method in the Compose source code.
Box(modifier = Modifier.semantics {
role = Role.Button
})
Accessibility role - iOS
On iOS, the accessibilityTraits
attribute is used to indicate an accessibility role. The UIAccessibilityTraits
structure contains all options, such as header
, button
, link
and image
, among others.
You can also combine multiple traits. For example, for a selected button you can can pass both traits as an array: [.button, .selected]
.
element.accessibilityTraits = .button
element.accessibilityTraits = .header
element.accessibilityTraits = .link
element.accessibilityTraits = .image
element.accessibilityTraits = [.button, .selected]
Accessibility role - SwiftUI
In SwiftUI, you can set an accessibility role by using the accessibility traits
on views. This is done by using the accessibilityAddTraits
modifier, which allows you to specify the role a view should play in the user interface. Traits can make a view act as a button, header, link, image, and more. You can also combine multiple traits to define complex roles.
// Button Trait
Text("Tap Me")
.accessibilityAddTraits(.isButton)
// Header Trait
Text("Section Header")
.font(.headline)
.accessibilityAddTraits(.isHeader)
// Link Trait
Text("Visit Website")
.foregroundColor(.blue)
.underline()
.accessibilityAddTraits(.isLink)
// Image Trait
Image(systemName: "star.fill")
.accessibilityAddTraits(.isImage)
// Combined Traits: Button and Selected
Text("Selected Button")
.accessibilityAddTraits([.isButton, .isSelected])
If you need to remove specific traits from a view, you can use the accessibilityRemoveTraits
modifier.
// View containing an image that acts as a decorative element
Image(systemName: "envelope")
.accessibilityRemoveTraits(.isImage)
Accessibility role - Flutter
For some widgets in Flutter, the role is assignd automatically. This happens, for example, with Flutter's buttons and text fields. If this is not the case, you can use Semantics
to indicate a role. The Semantics constructor
contains all available options, such as button
, header
, link
and image
, among others.
Semantics(
button: true,
header: true,
link: true,
image: true,
child: Widget(...)
);
Accessibility role - React Native
In React Native you can use the accessibilityRole
prop to set the accessibility role of an element. Available roles include button
, header
, link
and image
, among others.
<Pressable
accessibilityRole="button|header|link|image" />
Accessibility role - .NET MAUI
In MAUI, there is no built-in support for setting an accessibility role.
By intercepting the handler changed event, you can change the role of a custom component.
HandlerChanged event in XAML:
<StackLayout>
<BindableLayout.ItemTemplate>
<DataTemplate>
<controls:BorderedFrame
HandlerChanged="Frame_HandlerChanged">
<Grid...>
</Grid>
<Frame.GestureRecognizers>
<TapGestureRecognizer/>
</Frame.GestureRecognizers>
</controls:BorderedFrame>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
Component on Android:
public partial class Component
{
void Frame_HandlerChanged(System.Object sender, System.EventArgs e)
{
if (sender is Frame frame && frame.Handler?.PlatformView is Android.Widget.FrameLayout view)
{
ViewCompat.SetAccessibilityDelegate(view, new CustomFrameDelegate(ViewCompat.GetAccessibilityDelegate(view)));
}
}
}
public class CustomFrameDelegate : AccessibilityDelegateCompatWrapper
{
public CustomFrameDelegate(AccessibilityDelegateCompat? originalDelegate) : base(originalDelegate)
{
}
public override void OnInitializeAccessibilityNodeInfo(Android.Views.View host, AccessibilityNodeInfoCompat info)
{
base.OnInitializeAccessibilityNodeInfo(host, info);
if (info != null)
info.ClassName = "android.widget.Button";
}
}
Component on iOS:
public partial class Component
{
void Frame_HandlerChanged(System.Object sender, System.EventArgs e)
{
if (sender is Frame frame && frame.Handler != null)
{
var view = (UIView)frame.Handler.PlatformView!;
view.AccessibilityTraits = UIAccessibilityTrait.Button;
}
}
}
Accessibility role - Xamarin
Xamarin Forms does not have built-in support for setting an accessibility role.
By using Effects
it is possible to implement platform specific behaviour.
The SemanticEffect
file inside the Xamarin.CommunityToolkit
defines various methods to set accessibility roles.
<controls:CustomFontLabel
xct:SemanticEffect.HeadingLevel="1"
xct:SemanticEffect.Description="Button" />
Set a value. For a checkbox, the value is "selected" or "not selected". For a volume control, the value can be "50%". By setting a value, this can also be passed textually to tools.
Set accessibility value
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility value - Android
Android has limited support to provide a dedicated accessibility value for assistive technologies. The AccessibilityNodeInfoCompat
object contains a couple of methods, such as the setChecked
method.
Unfortunately the desired value is often not available. If your desired value is not included, you can append it to the contentDescription
attribute.
ViewCompat.setAccessibilityDelegate(
element,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.isChecked = true
}
}
)
element.contentDescription = "Name (Value)"
Accessibility value - Jetpack Compose
In Jetpack Compose, you can use the clearAndSetSemantics
to override existing semantics and set new properties.
// Override so label and value will be read out together
val label = "Text label"
val value = "Text value"
Text(
text = value,
modifier = Modifier.clearAndSetSemantics {
text = AnnotatedString("$label, $value")
}
)
Accessibility value - iOS
On iOS, you can set an accessibility value with the accessibilityValue
or accessibilityAttributedValue
property.
When using the semantically correct element, you usually do not need to modify the accessibilityValue
. For example, a UISwitch
sets the accessibilityValue
to selected
or not selected
and a UISlider
sets the accessibilityValue
to the current value. If the default value is incorrect or unclear, you can override the value manually.
element.accessibilityValue = "Custom"
Accessibility value - SwiftUI
In SwiftUI, you can set an accessibility value with the accessibilityValue
view modifier by providing a Text
value description.
When using the semantically correct element, you usually do not need to modify the accessibilityValue
. For example, a Toggle
sets the accessibilityValue
to On
or Off
and a Slider
sets the accessibilityValue
to the current value. If the default value is incorrect or unclear, you can override the value manually.
@State private var progress: Double = 0
var body: some View {
CustomSlider(value: $progress)
.accessibilityValue("\(progress)")
}
Accessibility value - Flutter
With Flutter, you can set an accessibility value by using the value
or attributedValue
property of Semantics
.
When using the semantically correct element, you usually do not need to modify the accessibility value. For example, Slider
, Switch
and CheckBox
, and others automatically assign accessibiluty values.
It is also possible to set an increasedValue
and decreasedValue
or attributedDecreasedValue
and attributedIncreasedValue
to indicate what the value will become when the user decreases or increases the value.
Some widgets include additional methods, such as semanticFormatterCallback
.
Semantics(
value: 'Custom',
increasedValue: 'Custom + 1',
decreasedValue: 'Custom - 1',
child: Widget(),
);
Accessibility value - React Native
In React Native you can use the accessibilityValue
and accessibilityState
props to set an accessibility value. The accessibilityValue
indicates the current value of a component. You can indicate a range, using min
, max
, and no
, or text using text
. The accessibilityState
indicates the current state of a component, for example disabled
or checked
.
<View
accessibilityValue={{min: 0, max: 100, now: 50}}
accessibilityState="busy" />
<View
accessibilityValue={{text: "Custom"}}
accessibilityState="disabled" />
Accessibility value - .NET MAUI
In MAUI, elements such as Button
and Entry
automatically include an accessibility value. When you create custom elements you have to set these properties yourself.
However, there is no dedicated property to set an accessibility value. You can embed the value inside the label by using MultiBinding
inside the SemanticProperties.Description
property.
<Label>
<SemanticProperties.Description>
<MultiBinding StringFormat="{}{0}, {1}">
<Binding Source="The value is: " />
<Binding Source="{BindingValue}" />
</MultiBinding>
</SemanticProperties.Description>
</Label>
Accessibility value - Xamarin
Xamarin Forms elements such as Button
and Entry
automatically include an accessibility value. When you make custom elements you have to set these properties yourself.
However, there is no dedicated property to set an accessibility value. You can embed the value inside the label by using MultiBinding
inside the AutomationProperties.Name
property.
<Label
<AutomationProperties.Name>
<MultiBinding StringFormat="{}{0}, {1}">
<Binding Source="The value is: " />
<Binding Source="{BindingValue}" />
</MultiBinding>
</AutomationProperties.Name>
</Label>
4. Make sure text can be resized
Is it possible to display text in a larger font size? Does text remain readable with a larger font size?
Ensure that the text in your app supports resizing. Users specify their preferred font size in the system settings. Text in your app should resize according to the preferred font size. This is especially important for visually impaired users because otherwise they might not be able to read the text. Text should not be abbreviated with dots.
Support text scaling
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Scale text - Android
On Android, you can use Scale-independent Pixels to scale text. This unit ensures that the user's preferences are taken into account when determining the font size. We recommend to define the textSize
in your styles to make sure it's the same everywhere.
<style name="Widget.TextView">
<item name="android:textSize">18sp</item>
</style>
Scale text - Jetpack Compose
In Jetpack Compose, you can use Scale-independent Pixels to scale text. This unit ensures that the user's preferences are taken into account when determining the font size. We recommend to define the fontSize
property inside the Typography
object in your code to ensure consistency throughout your app.
You can use the @PreviewFontScale
annotation to preview different font scales.
val typography = Typography(
titleLarge = TextStyle(
fontSize = 20.sp,
),
bodyLarge = TextStyle(
fontSize = 16.sp,
),
headlineLarge = TextStyle(
fontSize = 20.sp,
),
)
@FontScalePreviews
@Composable
fun fontScalePreviews() {
Text(text = "This is font scale ${LocalDensity.current.fontScale}")
}
Scale text - iOS
On iOS, you can use Dynamic Type
to scale text. By using this function, the font size is adjusted to the preferences of the user. If you're using your own font, you can use the scaledFont
method from UIFontMetrics
to calculate the font size.
Text elements such as UILabel
, UITextField
and UITextView
have a property called adjustsFontForContentSizeCategory
. If you set it to true
, the element automatically updates its font when the device's content size category changes.
For adjustsFontForContentSizeCategory
to take effect, the element’s font must be one of the following:
A font vended using
preferredFont(forTextStyle:)
orpreferredFont(forTextStyle:compatibleWith:)
with a validUIFont.TextStyle
A font vended using
UIFontMetrics.scaledFont(for:)
or one of its variants
// MARK: - Scaling custom fonts
import UIKit
extension UIFont {
static func font(name: String, size: CGFloat, style: TextStyle) -> UIFont {
guard let font = UIFont(name: name, size: size) else {
fatalError("Font \(name) does not exist")
}
return UIFontMetrics(forTextStyle: style).scaledFont(for: font)
}
static func openSans(weight: UIFont.Weight, size: CGFloat, style: TextStyle) -> UIFont {
if UIAccessibility.isBoldTextEnabled {
return font(name: "OpenSans-Bold", size: size, style: style)
}
switch weight {
case .regular:
return font(name: "OpenSans-Regular", size: size, style: style)
case .semibold:
return font(name: "OpenSans-SemiBold", size: size, style: style)
case .bold:
return font(name: "OpenSans-Bold", size: size, style: style)
default:
fatalError("Font weight \(weight) is not supported")
}
}
}
// MARK: - Enabling content size category adjustments
label.adjustsFontForContentSizeCategory = true
Scale text - iOS
In SwiftUI, scaling text to match the user's preferred content size is straightforward. SwiftUI automatically supports Dynamic Type
, which means your text will adapt to the user's preferred font size set in the device settings.
Text("Appt")
// Scales text automatically
.font(.title)
You can also customize the scaling behavior if you're using custom fonts or want more control. Use the custom(_:size:relativeTo:)
modifier method to ensure the text scales properly.
var body: some View {
Text("Appt")
.scaledFont(name: "Roboto-Regular", size: 24)
}
struct ScaledFont: ViewModifier {
var name: String
var size: CGFloat
var relativeTo: Font.TextStyle
func body(content: Content) -> some View {
content
.font(.custom(name,
size: size,
relativeTo: relativeTo))
}
}
extension View {
func scaledFont(name: String,
size: CGFloat,
relativeTo: Font.TextStyle = .body) -> some View {
self.modifier(ScaledFont(name: name,
size: size,
relativeTo: relativeTo))
}
}
Scale text - Flutter
Flutter automatically scales the text on the screen to the text size set by the user. We recommend using ThemeData
to use the same text sizes and fonts everywhere.
Try to avoid using the textScaleFactor
property because it overrides the text scale factor preferred by the user. The default factor is 1.0
, but can go as high as 4.0
for some users. Restricting the number means that some users might not be able to read the text.
There are valid use cases to restrict the textScaleFactor
to a certain number. You can use MediaQuery
to override the value globally. You can also override it for a single use case by using the property inside a Text
widget.
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 1.0, // Override scale for all widgets
),
child: ...,
);
Text(
'Appt',
textScaleFactor: 1.0, // Override scale for a single widget
);
Scale text - React Native
React Native automatically scales text depending on the font size preferences of the user settings. In addition, all dimensions in React Native are unitless, and represent density-independent pixels.
Try to avoid using properties such as maxFontSizeMultiplier
, allowFontScaling
, adjustsFontSizeToFit
and numberOfLines
. Using these properties may cause text to be unscalable or become inaccessible.
When inheriting a project you may find previous developers have disabled font-scaling with the following code: Text.defaultProps.allowFontScaling = false;
. This is accessibility anti-pattern and should be rolled back.
The code example below shows how to have a scaling font size.
<Text style={{ fontSize: 16 }}>
Appt
</Text>
Scale text - .NET MAUI
In mAUI, all controls that display text automatically apply font scaling. The scale is based on the font size preference set in the Android or iOS operating system.
By default, .NET MAUI apps use the Open Sans
font on each platform. However, this default can be changed by registering additional fonts in your app.
You can find additional guidance in the .NET MAUI fonts article.
<Label Text="Appt"
FontSize="18"
FontAutoScalingEnabled="True"
FontFamily="Custom" />
Scale text - Xamarin
In Xamarin Forms you make styles for the scalable fonts that you use in your app.
First, accessibility scaling should be enabled for named font sizes. This can be done by pass True
to the the SetEnableAccessibilityScalingForNamedFontSizes
method of the Application
. This can also be done in XAML by using ios:Application.EnableAccessibilityScalingForNamedFontSizes="true"
.
Secondly, you have to register the font and it's properties with the assembly. Afterwards, the fonts can be used in your app and they will automatically scale depending on the users' font size preference.
For more information, see Understand named font sizes, Named font size scaling and Dynamic Styles.
The code examples below shows how to enable font size scaling and how to use dynamic styles.
using Xamarin.Forms;
[assembly: ExportFont("Lobster-Regular.ttf", Alias="Lobster")]
[assembly: ExportFont("Lobster-Bold.ttf", Alias="LobsterBold")]
namespace Project
{
public partial class App : Xamarin.Forms.Application
{
On<Xamarin.Forms.PlatformConfiguration.iOS>().SetEnableAccessibilityScalingForNamedFontSizes(true);
}
}
<Application
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
ios:Application.EnableAccessibilityScalingForNamedFontSizes="true">
</Application>
<Style TargetType="Entry">
<Setter Property="FontFamily" Value="Lobster" />
</Style>
<Style
x:Key="LabelRegular"
ApplyToDerivedTypes="True"
BaseResourceKey="BodyStyle"
TargetType="Label">
<Setter Property="TextColor" Value="Black" />
<Setter Property="FontFamily" Value="Lobster" />
<Setter Property="FontSize" Value="{DynamicResource Body}" />
<!-- For Android you have to set FontSize property -->
</Style>
<Style
x:Key="LabelBold"
ApplyToDerivedTypes="True"
BaseResourceKey="LabelRegular"
TargetType="Label">
<Setter Property="FontFamily" Value="LobsterBold" />
<Setter Property="FontAttributes">
</Style>
Ensure that all content on the screen remains readable even with the largest font. Content should be readable without having to scroll in two directions. Because the text is displayed larger, it can push other elements off the screen. Ensure content can still be reached, for example, by scrolling vertically.
Prevent text truncation
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Text truncation - Android
On Android, you can avoid text truncation by removing all instances of android:maxLines
from your app. You should also avoid using fixed values for any heights or widths and instead use wrap_content
where possible.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Avoid text truncation"
android:maxLines="REMOVE" />
Text truncation - Jetpack Compose
In Jetpack Compose, you can avoid text truncation by removing all instances of maxLines
from your app. You should also avoid using fixed values for any heights or widths.
Text(
text = "Appt",
maxLines = 1 // Do not set maxLines
)
Column(
modifier = Modifier
.width(100.dp) // Do not use fixed width
.height(100.dp) // Do not use fixed height
) {
// Content
}
Text truncation - iOS
On iOS, you can avoid text truncation by seting the numberOfLines
property to 0
on your UILabel
. You should also avoid using fixed values for any heights or widths and instead use constraints
and self-sizing
.
let label = UILabel()
label.text = "Avoid text truncation"
label.numberOfLines = 0
Text truncation - SwiftUI
In SwiftUI, Text
views expand to multiple lines by default. To ensure that Text
views display without truncation, verify that you haven't set a lineLimit
.
To further avoid text truncation, especially when the text content exceeds the available screen space, it's recommended to place the Text
view inside a scrollable container like a ScrollView
.
ScrollView {
VStack {
Text("Appt")
// Other views
}
}
Text truncation - Flutter
In Flutter, you can avoid text truncation by removing all instances of maxLines
from your app. You should also set overflow
to TextOverflow.visible
where needed. Lastly, avoid using fixed values for any heights or widths.
Text(
'Avoid text truncation',
maxLines: REMOVE,
overflow: TextOverflow.visible
)
Text truncation - React Native
In React Native, you can avoid text truncation by removing all instances of numberOfLines
from you rapp.
<Text numberOfLines="{REMOVE}">
Avoid text truncation
</Text>
Text truncation - .NET MAUI
In MAUI, the Label
component has the MaxLines
property set to -1
by default, making labels not truncated. You can modify this behavior by changing the MaxLines
property to a specific number.
<Label
Text="Avoid text truncation"
MaxLines="-1" />
Text truncation - Xamarin
When using Xamarin.Forms, you can avoid text truncation by removing all instances of MaxLines
from your app.
<Label
Text="Avoid text truncation"
MaxLines="REMOVE" />
5. Provide sufficient contrast
Does text have sufficient contrast relative to the background? Do elements on the screen have sufficient contrast against each other and in relation to the background?
Ensure that the content on the screen has a contrast of at least 3:1 with the surrounding color. Think of graphic elements such as icons, buttons and input fields.
Ensure that the contrast ratio between the text color and background color is at least 4.5:1. For bold and large text, a ratio of 3:1 is sufficient.
By maintaining these ratios, visually impaired and color blind users can usually read the text well. In addition, this makes an app easier for everyone to use, for example outside in the sun.
6. Show focus and offer a logical order
Is it clear where the focus is? Is the order in which the focus moves logical?
Ensure that elements focused by assistive technologies are clearly indicated. Focus is often shown by placing a box around the element. Make sure that the placement is correct and that the color is clearly visible. For apps it is not possible to adjust the color of the frame. However, it is possible to give elements a different background color when they have focus.
Add accessibility focus indicator
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility focus indicator - Android
On Android, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings of Android.
You can use a ColorStateList
to change colors based on the element state. An element moves into the state_focused
whenever it receives focus.
The code sample below shows how to change the background color of a button on focus.
<!-- selector.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/focused" android:state_focused="true" />
<item android:drawable="@color/default" />
</selector>
<!-- layout.xml -->
<Button
android:id="@+id/button"
android:background="@drawable/selector">
</Button>
Accessibility focus indicator - Jetpack Compose
In Jetpack Compose, you can easily create a focus indicator by using either the border
property or the border
modifier.
Another way, is by implementing a custom Indication
. You can read official documentation for more details.
var color by remember { mutableStateOf(Color.White) }
Card(
modifier = Modifier
.onFocusChanged {
// Change border color depending on focus state
color = if (it.isFocused) Color.Green else Color.White
}
.border(3.dp, color)
) {
// Card content...
}
Accessibility focus indicator - iOS
On iOS, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings of iOS.
You can override the accessibilityElementDidBecomeFocused
and accessibilityElementDidLoseFocus
methods to listen to focus state changes. By subclassing an element, you can change the colors based on the element state.
The code sample below shows how to change the background color of a button on focus.
class Button: UIButton {
override open func accessibilityElementDidBecomeFocused() {
backgroundColor = .focused
}
override open func accessibilityElementDidLoseFocus() {
backgrounColor = .default
}
}
Accessibility focus indicator - SwiftUI
On iOS, while it's not possible to alter the built-in focus indicators used by assistive technologies, you can visually enhance the accessibility of user interface elements by changing their appearance when they receive focus. This customization can help users better identify which element is active, especially those relying on visual cues.
In SwiftUI, you can dynamically change the visual appearance of views based on their focus state. For example, using the @AccessibilityFocusState
property, you can track whether an element is focused and apply visual changes, such as altering the border color. This allows you to provide a clear, visual indicator of focus, improving the accessibility and usability of your app.
@AccessibilityFocusState private var isEmailFocused: Bool
@State private var email = ""
var body: some View {
Form {
TextField("Email", text: $email, prompt: Text("Email"))
// Binds the focus state of this TextField to the 'isEmailFocused' property.
.accessibilityFocused($isEmailFocused)
// The border color changes based on whether the TextField is focused:
.border(isEmailFocused ? .red : .clear)
}
}
Accessibility focus indicator - Flutter
In Flutter, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings on Android and iOS.
You can change colors based on MaterialState
. For a button, you could add a ButtonStyle
to change the color when in .focused
state.
The code sample below shows how to change the background color of a button on focus.
TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith(getColor),
),
child: Text('Button'),
);
Color? getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue;
}
return Colors.red;
}
Accessibility focus indicator - React Native
In React Native, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings on Android and iOS.
Not available, contribute!
Accessibility focus indicator - .NET MAUI
In MAUI, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings on Android and iOS.
The Visual State Manager
(VSM) provides a structured way to make visual changes to the user interface from code. The VSM
contains a visual state group named CommonStates
which includes the Focused
state.
The code sample below shows how to change the background color of an entry on focus.
<Style TargetType="Entry">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="DefaultColor" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="FocusedColor" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="DisabledColor" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
Accessibility focus indicator - Xamarin
In Xamarin.Forms, you can adjust colors when an element receives focus. However, it's not possible to change the focus indicator of assistive technologies. Users can adjust their preferences in the system settings on Android and iOS.
The Visual State Manager
(VSM) provides a structured way to make visual changes to the user interface from code. The VSM
contains a visual state group named CommonStates
which includes the Focused
state.
The code sample below shows how to change the background color of a button on focus.
<Style TargetType="Button">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="DefaultColor" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="FocusedColor" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="DisabledColor" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
Ensure assistive technologies use a logical focus order when navigating. The order of navigating a screen is usually from left to right, from top to bottom. Make sure assistive technologies use an equivalent focus order.
Adjust order for keyboard
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Keyboard order - Android
On Android, you can use several focus
properties to modify the keyboard focus order.
android:nextFocusForward
: set the next element to move focus to.android:nextFocusUp
: specify which element should receive focus when navigating upandroid:nextFocusDown
: specify which element should receive focus when navigating downandroid:nextFocusLeft
: specify which element should receive focus when navigating to the leftandroid:nextFocusRight
: specify which element should receive focus when navigating to the right
<View
android:id="@+id/notFocusable"
android:focusable="false"/>
<EditText
android:id="@+id/field1"
android:focusable="true"
android:nextFocusForward="@+id/field2"
android:nextFocusDown="@+id/field3"
android:nextFocusRight="@+id/field2"/>
<EditText
android:id="@+id/field2"
android:focusable="true"
android:nextFocusForward="@+id/field3"
android:nextFocusDown="@+id/field4"/>
<EditText
android:id="@+id/field3"
android:focusable="true"
android:nextFocusForward="@+id/field4"/>
<EditText
android:id="@+id/field4"
android:focusable="true"/>
Keyboard order - Jetpack Compose
In Jetpack Compose, to change the default focus traversal order for navigation, you can use the focusProperties
modifier to specify the item that should receive focus when navigating up, down, or in any other direction.
You can use the following focusProperties
:
next
: specifies which element should receive focus when navigating to the next.previous
: specifies which element should receive focus when navigating to the previous.up
: specifies which element should receive focus when navigating up.down
: specifies which element should receive focus when navigating down.left
: specifies which element should receive focus when navigating to the left.right
: specifies which element should receive focus when navigating to the right.start
: specifies which element should receive focus when navigating to the left in LTR mode and right in RTL mode.end
: specifies which element should receive focus when navigating to the right in LTR mode and left in RTL mode.
// Create set of reference for each Composable
val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
Button(
onClick = { },
modifier = Modifier
.focusRequester(fourth)
.focusProperties {
down = third
right = second
}
) {
// Button content...
}
Keyboard order - iOS
On iOS, you can use the accessibilityRespondsToUserInteraction
attribute to optimize keyboard navigation. By setting the property to false
, the element will be skipped with keyboard navigation. Other assistive technologies, such as VoiceOver can still focus on the element. This way you can provide screen reader users with alternative text for images, but skip focus for keyboard users. When a hardware keyboard is connected and VoiceOver is enabled, the image will be focusable.
For even more concise control over the keyboard order, you can use properties such as canBecomeFocused
,focusGroupIdentifier
and focusGroupPriority
.
To debug focus, you can use UIFocusDebugger
.
A use case could be a grid where you want to navigate by rows. You can achieve this by setting the same focusGroupIdentifier
for each column in a row.
grid.topLeft.focusGroupIdentifier = "top"
grid.topRight.focusGroupIdentifier = "top"
grid.bottomLeft.focusGroupIdentifier = "bottom"
grid.bottomRight.focusGroupIdentifier = "bottom"
Keyboard order - SwiftUI
In SwiftUI, you can use the accessibilityRespondsToUserInteraction
view modifier to optimize keyboard navigation. By setting the property to false
, the element will be skipped with keyboard navigation. Other assistive technologies, such as VoiceOver can still focus on the element. This way you can provide screen reader users with alternative text for images, but skip focus for keyboard users. When a hardware keyboard is connected and VoiceOver is enabled, the image will be focusable.
For more precise control over the keyboard order, you can use accessibilitySortPriority
view modifier to specify the priority of the item when using Full Keyboard Access
.
VStack {
HStack {
Text("Top Left")
// Focused first
.accessibilitySortPriority(4)
// Allow interaction using Full Keyboard Access
.accessibilityRespondsToUserInteraction(true)
Text("Top Right")
// Focused third
.accessibilitySortPriority(2)
.accessibilityRespondsToUserInteraction(true)
}
HStack {
Text("Bottom Left")
// Focused second
.accessibilitySortPriority(3)
.accessibilityRespondsToUserInteraction(true)
Text("Bottom Right")
// Not focusable by Full Keyboard Access
.accessibilityRespondsToUserInteraction(false)
}
}
Keyboard order - Flutter
In Flutter, you can use FocusTraversalGroup
to group widgets together. All subwidgets must be fully traversed before the keyboard focus is moved to the next widget. When grouping widgets into related groups is not enough, a FocusTraversalPolicy
can be set to determine the ordering within the group.
The default ReadingOrderTraversalPolicy
is usually sufficient, but in cases where more control over ordering is needed, an OrderedTraversalPolicy
can be used. The order
argument of the FocusTraversalOrder
widget wrapped around the focusable components determines the order. The order can be any subclass of FocusOrder
, but NumericFocusOrder
and LexicalFocusOrder
are provided.
Read more about Flutter's keyboard focus system.
Full Keyboard Access (FKA) on iOS is not yet fully supported on Flutter.
February 21, 2021: Flutter issue regarding FKA has been created
October 24, 2024: Bare-bones FKA implementation has been merged
The example below shows how to use the FocusTraversalOrder
widget to traverse a row of buttons in the order TWO, ONE, THREE using NumericFocusOrder
.
Row(
children: <Widget>[
FocusTraversalOrder(
order: NumericFocusOrder(2.0),
child: TextButton(
child: const Text('ONE'),
),
),
const Spacer(),
FocusTraversalOrder(
order: NumericFocusOrder(1.0),
child: TextButton(
child: const Text('TWO'),
),
),
const Spacer(),
FocusTraversalOrder(
order: NumericFocusOrder(3.0),
child: TextButton(
child: const Text('THREE'),
),
),
],
);
Keyboard order - React Native
React Native has implemented all Android keyboard focus properties.
nextFocusForward
: specify the next element to move focus tonextFocusUp
: specify which element should receive focus when navigating upnextFocusDown
: specify which element should receive focus when navigating downnextFocusLeft
: specify which element should receive focus when navigating leftnextFocusRight
: specify which element should receive focus when navigating right
It seems that none of the iOS keyboard focus properties have been implemented by React Native.
Not available, contribute!
Keyboard order - .NET MAUI
In MAUI, there is no built-in way to set the order, but you can use the SemanticOrderView
from the MAUI Community Toolkit
.
XAML Config
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
<toolkit:SemanticOrderView x:Name="SemanticOrderView">
<VerticalStackLayout>
<Entry x:Name="EmailEntry" />
<Entry x:Name="PasswordEntry" IsPassword="True" />
</VerticalStackLayout>
</toolkit:SemanticOrderView>
And in code behind set the order
SemanticOrderView.ViewOrder = new List<View> { EmailEntry, PasswordEntry };
Keyboard order - Xamarin
Xamarin Forms supports changing the keyboard order through the TabIndex
property. The default value is 0. The lower the value, the higher the priority.
The IsTabStop
property can be used to exclude elements from tabbed navigation.
Read more about Keyboard Accessibility in Xamarin.Forms.
The code example below shows how exclude the label from receiving keyboard focus, and how to reach the save button before reaching the cancel button.
<Label Text="Appt" IsTabStop="True" />
<Button x:Name="cancelButton" TabIndex="20" />
<Button x:Name="saveButton" TabIndex="10"/>
Adjust order for assistive technologies
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility order - Android
On Android, you can set the accessibility order in XML, or modify the accessibility order in code. You can use the android:accessibilityTraversalAfter
and and android:accessibilityTraversalBefore
properties in XML. Or you can use the setAccessibilityTraversalBefore
and setAccessibilityTraversalAfter
methods in code.
<TextView
android:id="@+id/header" />
<RecyclerView
android:id="@+id/list"
android:accessibilityTraversalAfter="@id/description" />
<TextView
android:id="@+id/description"
android:accessibilityTraversalBefore="@id/header" />
header.setAccessibilityTraversalBefore(R.id.description)
list.setAccessibilityTraversalAfter(R.id.description)
Accessibility order - Jetpack Compose
In Jetpack Compose, you can use the traversalIndex
to alter the focus order of the screen.
The traversalIndex
will give assistive technologies an explicit order of traversing.
When a section of a screen is read out in an incorrect order, start by adding isTraversalGroup
to the parent Column
, Row
or Box
.
This will let assistive technologies know that this section is grouped and should be traversed, before moving on to a next section.
Then add traversalIndex
to elements in this group to fix any issues with the focus order.
It is possible to use isTraversalGroup
and traversalIndex
on the same element.
Box(modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = -1f
}) {
// Box content...
}
Accessibility order - iOS
On iOS, you can use the accessibilityElements
property to set the order for assistive technologies. Be careful using the accessibilityElements
property, because any elements left out of the array cannot be reached with assistive technologies.
view.accessibilityElements = [header, description, list]
Accessibility order - SwiftUI
In SwiftUI, assistive technology like VoiceOver
typically reads elements in a top-left to bottom-right order. However, you can customize this reading order using the accessibilitySortPriority
view modifier. A higher priority value means the element is read earlier. Use .accessibilityElement(children: .contain)
to group and manage the accessibility elements within stacks (HStack
, VStack
, or ZStack
) to improve navigation when using assitive technologies.
VStack {
Text("First Element")
.accessibilitySortPriority(2) // Reads second
Text("Second Element")
.accessibilitySortPriority(3) // Reads first
Text("Third Element")
.accessibilitySortPriority(1) // Reads third
}
.accessibilityElement(children: .contain) // Groups the stack's children
Accessibility order - Flutter
For sorting the focus order in Flutter apps, the sortKey
parameter of Semantics
is used.
This parameter uses a SemanticsSortKey
to sort the elements. The most common way to sort elements is the OrdinalSortKey
, but you can also write your own implementation based on the SemanticsSortKey
class.
The OrdinalSortKey
needs an order
as double and optionally a name
as String. The elements are then sorted by name, with empty names handled first, and subsequently by order
.
It is also possible to leave out the sortKey
. In this case, Flutter will generate OrdinalSortKey
's based on a platform specific algorithm. This order is often the order you want, but make sure to verify the sequence by using an assistive technology such as the screen reader.
Widget focusOrderWidget(context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Semantics(
sortKey: OrdinalSortKey(2.0),
child: Text("Second focus")
),
Semantics(
sortKey: OrdinalSortKey(1.0),
child: Text("First focus")
)
],
)
)
);
}
Accessibility order - React Native
React Native does not have support for changing the focus order. You can use the accessible
prop to indicate that a view should be focusable. The child elements get grouped together.
More information about the lack of support for changing accessibility order can be found inside Discussion 389 of the React Native Community.
Not available, contribute!
Accessibility order - .NET MAUI
In MAUI, you can use a SemanticOrderView
to control the order of VisualElements
for screen readers. This can be particularly useful when building user interfaces in orders differing from the order in which users and screen readers will navigate them.
For more information, check out the SemanticOrderView
documentation.
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CommunityToolkit.Maui.Sample.Pages.Views.SemanticOrderViewPage"
Title="Semantic Order View">
<ContentPage.Content>
<toolkit:SemanticOrderView x:Name="SemanticOrderView">
<Grid RowDefinitions="*,2*,*">
<Label Grid.Row="0" x:Name="DescriptionLabel" Text="Label for description, first label in xaml file" />
<Label Grid.Row="1" x:Name="TitleLabel" Text="Title, second label in xaml file" FontSize="30" />
<Label Grid.Row="2" Text="This label is excluded in the accessibility tree on iOS" />
</Grid>
</toolkit:SemanticOrderView>
</ContentPage.Content>
</ContentPage>
using System.Collections.Generic;
namespace CommunityToolkit.Maui.Sample.Pages.Views;
public partial class SemanticOrderViewPage : ContentPage
{
public SemanticOrderViewPage()
{
InitializeComponent();
this.SemanticOrderView.ViewOrder = new List<View> { TitleLabel, DescriptionLabel };
}
}
Note:
The
SemanticOrderView
is part of theCommunityToolkit.Maui
package.On iOS, only the labels listed in the
ViewOrder
will be in the accessibility tree, in the specified order. A label without aName
will not be accessible. If you want the last label to be accessible, it must have a name inXAML
and must be added to theViewOrder
property of theSemanticOrderView
.On Android, all labels are included in the accessibility tree. The order is:
TitleLabel
,DescriptionLabel
, and lastly the label without a name.
Accessibility order - Xamarin
Xamarin Forms supports changing the accessibility order through the TabIndex
property. The default value is 0. The lower the value, the higher the priority. For example, to reach the save button before reaching the cancel button, the cancel button's TabIndex
needs to be higher than the save button.
<Label Text="Appt" />
<Button x:Name="cancelButton" TabIndex="20" />
<Button x:Name="saveButton" TabIndex="10"/>
Move accessibility focus
- Android
- Jetpack Compose
- iOS
- SwiftUI
- Flutter
- React Native
- .NET MAUI
- Xamarin
Accessibility focus - Android
On Android, you can send an AccessibilityEvent
of the type TYPE_VIEW_FOCUSED
to move the focus of assistive technologies to a specific view. The view must be focusable for this event to take effect.
fun focus(view: View) {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
Accessibility focus - Jetpack Compose
In Jetpack Compose, to request focus for a Composable
, you need to attach the focusRequester
, and then call it from the desired place. The Composable
must be focusable for this to take effect.
// Request focus on Composition start
val focusRequester = remember { FocusRequester() }
TextField(
// ... textField setup
modifier = Modifier.focusRequester(focusRequester)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Accessibility focus - iOS
On iOS, you can use UIAccessibility
to post
a notification to move the focus of assistive technologies. Use screenChanged
when a new view appears that occupies a major portion of the screen. Otherwise, use layoutChanged
when the layout of current screen changes.
func focus(_ view: UIView) {
UIAccessibility.post(notification: .layoutChanged, argument: view)
UIAccessibility.post(notification: .screenChanged, argument: view)
}
Accessibility focus - SwiftUI
In SwiftUI, you can enhance accessibility focus by using the @AccessibilityFocusState
property wrapper and the accessibilityFocused
modifier. These methods allow you to programmatically move the accessibility focus to a specific element in your app. This can be particularly useful when you want to direct the user's attention to a specific part of the UI in response to changes or interactions.
// State variable to control loading state
@State var isLoading: Bool = false
// Accessibility focus state variable to manage focus
@AccessibilityFocusState var isLoadingIndicatorFocused: Bool
var body: some View {
VStack {
Button("Search Appt website") {
// Set loading state to true
isLoading = true
// Move focus to the loading indicator
isLoadingIndicatorFocused = true
}
if isLoading {
ProgressView()
.accessibilityFocused($isLoadingIndicatorFocused) // Bind the focus state
.accessibilityLabel("Loading") // Provide an accessible label
}
}
}
Accessibility focus - Flutter
In Flutter, you can use a FocusSemanticEvent
to move the accessibility focus.
This API is generally not recommended because it can disrupt users' expectations of accessibility focus.
It should be used carefully and only in specific cases, like replacing a focused rendering object with another, though such designs should generally be avoided.
Note: do not use FocusNode
or Semantics.focused
, these methods should only be used for keyboard or input focus.
class ApptWidget extends StatelessWidget {
final GlobalKey _key = GlobalKey();
Widget build(BuildContext context) {
// Ensure focus change occurs after rendering.
WidgetsBinding.instance.addPostFrameCallback((_) {
_key.currentContext
?.findRenderObject()
?.sendSemanticsEvent(const FocusSemanticEvent());
});
return Text('FocusSemanticEvent', key: _key);
}
}
Accessibility focus - React Native
In React Native you can move accessibility focus by using the setAccessibilityFocus
method from the AccessibilityInfo class
. This method requires a reactTag
, which you can find by calling the findNodeHandle
method.
function Component() {
const ref = useRef(null);
function setFocus() {
const reactTag = findNodeHandle(ref.current);
if (reactTag) {
AccessibilityInfo.setAccessibilityFocus(reactTag);
}
}
return <View ref={ref} accessible accessibilityLabel="Modal" />
};
Accessibility focus - .NET MAUI
In MAUI, the SemanticExtensions
class contains the SetSemanticFocus
method. This method moves the accessibility focus to the given element on the native platform.
The code sample below shows how to move the accessibility focus to a specific element.
<HorizontalStackLayout
x:Name="MenuButtonLayout"
AutomationId="MenuButton"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="Menu container"
Padding="0,0,5,0">
<Button
Text="Click to set semantic focus to the label below"
Clicked="SetSemanticFocus_Clicked"/>
<Label
x:Name="semanticFocusLbl"
Text="Label to set semantic focus"/>
</HorizontalStackLayout>
private void SetSemanticFocus_Clicked(object sender, System.EventArgs e)
{
semanticFocusLbl.SetSemanticFocus();
}
Accessibility focus - Xamarin
Xamarin Forms does not have built-in support for changing accessibility focus.
The SemanticExtensions
file inside the Xamarin.CommunityToolkit
contains the SetSemanticFocus
method. It moves the accessibility focus to the given element on the native platform.
SemanticExtensions.SetSemanticFocus(element)
Testing
You can test most of the steps with the Accessibility Scanner app and Xcode's Accessibility Inspector. You can also detect contrast issues automatically with these. another option is to take a screenshot of an app to determine the color codes and calculate the contrast. This can be done with WebAIM's Contrast Checker, for example.
In addition, check that it works properly by using the screen reader.
To test whether text scaling is properly supported, set a large font size. You can do this on a phone under the accessibility settings.
Questions?
By following these steps, you will make your app accessible to a very large group of people. Anything missing? Is something not quite clear? Let us know in the Slack channel of Appt.