15. Accessibility and internationalization

Goal

Why this matters

Prerequisites

Key namespaces

1. Keyboard accessibility

1.1 Focus order and tab navigation

<StackPanel Spacing="8" KeyboardNavigation.TabNavigation="Cycle">
  <TextBlock Text="_User name" RecognizesAccessKey="True"/>
  <TextBox x:Name="UserName" TabIndex="0"/>

  <TextBlock Text="_Password" RecognizesAccessKey="True"/>
  <PasswordBox x:Name="Password" TabIndex="1"/>

  <CheckBox TabIndex="2" Content="_Remember me"/>

  <StackPanel Orientation="Horizontal" Spacing="8">
    <Button TabIndex="3">
      <AccessText Text="_Sign in"/>
    </Button>
    <Button TabIndex="4">
      <AccessText Text="_Cancel"/>
    </Button>
  </StackPanel>
</StackPanel>

1.2 Keyboard navigation helpers

KeyboardNavigation (source: KeyboardNavigation.cs) provides:

2. Screen reader semantics

Attach AutomationProperties to expose names, help text, and relationships:

<StackPanel Spacing="10">
  <TextBlock x:Name="EmailLabel" Text="Email"/>
  <TextBox Text="{Binding Email}"
           AutomationProperties.LabeledBy="{Binding #EmailLabel}"
           AutomationProperties.AutomationId="EmailInput"/>

  <TextBlock x:Name="StatusLabel" Text="Status"/>
  <TextBlock Text="{Binding Status}"
             AutomationProperties.LabeledBy="{Binding #StatusLabel}"
             AutomationProperties.LiveSetting="Polite"/>
</StackPanel>

AutomationProperties map to automation peers. The base ControlAutomationPeer inspects properties and pseudo-classes to expose state.

3. Custom automation peers

Create peers when you author custom controls so assistive technology can identify them correctly.

public class ProgressBadge : TemplatedControl
{
    public static readonly StyledProperty<string?> TextProperty =
        AvaloniaProperty.Register<ProgressBadge, string?>(nameof(Text));

    public string? Text
    {
        get => GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    protected override AutomationPeer? OnCreateAutomationPeer()
        => new ProgressBadgeAutomationPeer(this);
}

public sealed class ProgressBadgeAutomationPeer : ControlAutomationPeer
{
    public ProgressBadgeAutomationPeer(ProgressBadge owner) : base(owner) { }

    protected override string? GetNameCore() => (Owner as ProgressBadge)?.Text;
    protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Text;
    protected override AutomationLiveSetting GetLiveSettingCore() => AutomationLiveSetting.Polite;
}

4. High contrast and theme variants

Avalonia supports theme variants (Light, Dark, HighContrast). Bind colors to resources instead of hard-coding values.

<ResourceDictionary>
  <ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Default">
      <SolidColorBrush x:Key="AccentBrush" Color="#2563EB"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="HighContrast">
      <SolidColorBrush x:Key="AccentBrush" Color="#00FF00"/>
    </ResourceDictionary>
  </ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

Switch variants for testing:

Application.Current!.RequestedThemeVariant = ThemeVariant.HighContrast;

Provide clear focus visuals using pseudo-classes (:focus, :pointerover) and ensure contrast ratios meet WCAG (4.5:1 for body text). For Windows, respect system accent colors by reading RequestedThemeVariant and SystemBarColor (Chapter 7).

5. Text input, IME, and text services

IME support matters for CJK languages and handwriting. TextInputMethodClient is the bridge between your control and platform IME surfaces. Text controls in Avalonia already implement it; custom text editors should derive from TextInputMethodClient (or reuse TextPresenter).

public sealed class CodeEditorTextInputClient : TextInputMethodClient
{
    private readonly CodeEditor _editor;

    public CodeEditorTextInputClient(CodeEditor editor) => _editor = editor;

    public override Visual TextViewVisual => _editor.TextLayer;
    public override bool SupportsPreedit => true;
    public override bool SupportsSurroundingText => true;
    public override string SurroundingText => _editor.Document.GetText();
    public override Rect CursorRectangle => _editor.GetCaretRect();
    public override TextSelection Selection
    {
        get => new(_editor.SelectionStart, _editor.SelectionEnd);
        set => _editor.SetSelection(value.Start, value.End);
    }

    public void UpdateCursor()
    {
        RaiseCursorRectangleChanged();
        RaiseSelectionChanged();
        RaiseSurroundingTextChanged();
    }
}

Configure text options with the attached TextInputOptions properties:

<TextBox Text="{Binding PhoneNumber}"
         InputMethod.TextInputOptions.ContentType="TelephoneNumber"
         InputMethod.TextInputOptions.ReturnKeyType="Done"
         InputMethod.TextInputOptions.IsCorrectionEnabled="False"/>

When you detect IME-specific behaviour, test on Windows (IMM32), macOS, Linux (IBus/Fcitx), Android, and iOS — each backend surfaces slightly different capabilities.

6. Localization workflow

6.1 Resource management

Use RESX resources or a localization service that surfaces culture-specific strings.

public sealed class Loc : INotifyPropertyChanged
{
    private CultureInfo _culture = CultureInfo.CurrentUICulture;
    public string this[string key] => Resources.ResourceManager.GetString(key, _culture) ?? key;

    public void SetCulture(CultureInfo culture)
    {
        if (_culture.Equals(culture))
            return;

        _culture = culture;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

Register in App.axaml and bind:

<Application.Resources>
  <local:Loc x:Key="Loc"/>
</Application.Resources>

<TextBlock Text="{Binding [Ready], Source={StaticResource Loc}}"/>

Switch culture at runtime:

var culture = new CultureInfo("fr-FR");
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = culture;
((Loc)Application.Current!.Resources["Loc"]).SetCulture(culture);

6.2 Formatting and layout direction

7. Fonts and fallbacks

Ensure glyph coverage with FontManagerOptions:

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .With(new FontManagerOptions
    {
        DefaultFamilyName = "Noto Sans",
        FontFallbacks = new[]
        {
            new FontFallback { Family = "Noto Sans Arabic" },
            new FontFallback { Family = "Noto Sans CJK SC" }
        }
    })
    .StartWithClassicDesktopLifetime(args);

8. Testing accessibility

Tips for a repeatable test loop:

Document gaps (e.g., missing peers, insufficient contrast) and track them like any other defect.

9. Practice exercises

  1. Annotate a settings page with AutomationProperties.Name, HelpText, and AutomationId; inspect the automation tree with DevTools and NVDA.
  2. Derive a custom AutomationPeer for a progress pill control, exposing live updates and value patterns, then verify announcements in a screen reader.
  3. Configure TextInputOptions for phone number input on Windows, Android, and iOS. Test with an IME (Japanese/Chinese) to ensure composition events render correctly.
  4. Localize UI strings into two additional cultures (e.g., es-ES, ar-SA), toggle FlowDirection, and confirm mirrored layouts do not break focus order.
  5. Set up FontManagerOptions with script-specific fallbacks and validate that Arabic, Cyrillic, and CJK text render without tofu glyphs.

Look under the hood (source bookmarks)

Check yourself

What's next