MarkdownViewer exposes the structure of the rendered document to screen readers, lets keyboard users reach and open every link and interactive element, ships default colours that meet WCAG 2.2 AA contrast, and switches to a high-contrast palette when the operating system asks for one. Screen reader and keyboard support don't change how the document looks or behaves with the mouse.
Avalonia forwards automation peers to each platform's accessibility API: UI Automation on Windows, NSAccessibility on macOS, and AT-SPI on Linux. Each rendered element reports the following role:
| Markdown | Reported as |
|---|---|
| The viewer itself | Document, named by AutomationProperties.Name or, if that isn't set, by the first heading |
# … ###### |
Text with AutomationProperties.HeadingLevel 1–6 |
| Paragraph | Text |
| Link | Hyperlink, a child of its paragraph, named by its text and invokable. An image inside a link, such as a badge, is read as its alt text. A link title ([text](url "title")) is its help text. |
| List / list item | List / ListItem. Bullets are hidden; numbers stay readable, since an ordered list can start at any number. |
Interactive task (AreTaskListsInteractive) |
CheckBox, named by the task's text, with its checked state |
Fold button of a heading (AreHeadingsFoldable) |
Button, named by the heading's text, that expands and collapses (whether the section is expanded is reported) |
| Table / header cell / body cell | Table / HeaderItem / DataItem |
| Blockquote, figure | Group. A figure is named by its caption. |
Alert (> [!NOTE]) |
Group, named by its title ("NOTE", "TIP", …), so the alert kind is announced as text and not only shown by colour |
Header of a foldable alert (> [!NOTE]-, UseExtendedAlertBlocks()) |
Button, named by the alert's title, that expands and collapses (whether the alert is expanded is reported) |
<details> (UseHtmlDetails()) |
Group, named by its summary. The summary is a Button, named by the same text, that expands and collapses the body and reports whether it is expanded. |
Thinking block (UseThinkingBlocks()) |
Group, named by its header ("Thinking…", "Thought for 12 seconds"). The header is a Button, named by the same text, that expands and collapses the reasoning and reports whether it is expanded. |
| Code block line numbers | Hidden, so only the code is read |
| Thematic break | Separator |
| Image | Image, named by its alt text. An image with empty alt text () is decorative and hidden, as browsers do with alt="", unless it can be zoomed. |
Zoomable image (IsImageZoomEnabled) |
Image, named by its alt text, with the help text "Zoom". A Tab stop, so never hidden, even without alt text. |
| Zoomable Mermaid diagram | As a zoomable image, named by its diagram source |
| Image lightbox | Image, named by the image's alt text (a diagram's source), or "Image" without one. Its buttons are named "Close", "Zoom out", "Zoom in", "Fit to view" and "Save as". |
| Image that could not be loaded | Image, named by its alt text, with the help text "Image could not be loaded"; hidden without alt text |
<kbd> (UseHtmlSubset()) |
Text, read as the key's text |
| YouTube embed | Hyperlink, named by its alt text, or by its URL when there is none |
Math formula (MarkView.Avalonia.Math) |
Image, named by its TeX source |
Mermaid diagram (MarkView.Avalonia.Mermaid) |
Image, named by its diagram source |
The help texts and the lightbox's fallback name are set by MarkdownTheme.axaml styles (Image.markdown-image-zoomable, Border.markdown-image-failed, .markdown-image-lightbox), as are the copy buttons' names ("Copy code", "Copy formula"), so they can be translated.
To give the document a name of your own, set AutomationProperties.Name:
<mv:MarkdownViewer AutomationProperties.Name="Release notes" Source="avares://MyApp/notes.md" />
The paragraph itself is still read as one string, links included.
Avalonia 12.1.3 has the following gaps:
The viewer is one Tab stop, followed by each link and each focusable control in the document, in reading order: a foldable alert's header, a <details> summary, a thinking block's header, a foldable heading's fold button, a code block's or a display formula's copy button, an interactive task's check box, a zoomable image or Mermaid diagram, a video embed. Enter or Space folds or unfolds a focused alert, <details> block, thinking block or heading section and ticks a focused task; Enter or Space on a copy button copies the code or the formula's TeX source, and on an image opens it in the lightbox. In the lightbox, Tab moves between the lightbox, its close button, its zoom buttons and its save button only; + and - zoom, 0 fits the picture again, the arrow keys pan it, Ctrl+S (Cmd+S on macOS) saves it, and Escape closes the lightbox and gives the focus back to the image.
| Key | Action |
|---|---|
Tab |
Move to the next link. Past the last one, focus leaves the viewer. |
Shift+Tab |
Move to the previous link, then back to the viewer. Entering the viewer this way lands on its last link. |
Enter |
Open the focused link, as a click does: #anchor links scroll to their target, and other links raise LinkClicked. |
| Menu key | Open the viewer's context menu; on a focused link, it offers to copy the link's address. |
↑ / ↓ |
Scroll the document by a line (← / → sideways, when it is wider than the viewer). |
Page Up / Page Down, Shift+Space / Space |
Scroll the document by a page. |
Home / End, with or without Ctrl |
Scroll to the top or the bottom of the document. |
The scrolling keys work while the viewer itself has the focus, as it does after a click in the document or after a jump to an anchor, and while a link or a control in the document has it, unless that control uses the key itself (Space ticks a focused task). They do nothing while the lightbox is open. A key for a direction the document cannot scroll in, such as any of them on a document that fits in the viewer, is left to the viewer's parents, so a page that holds the viewer scrolls instead.
After a jump to an anchor (an #anchor link clicked or opened with Enter, or ScrollToAnchor), the viewer keeps the focus and Tab goes on from the anchor, as in a browser: to the first link at or below it, and Shift+Tab to the last link above it. A click elsewhere in the document drops that starting point.
The focused link scrolls into view, and a 2px ring outlines each line of its text. The ring uses FluentTheme's SystemControlFocusVisualPrimaryBrush, or black/white without FluentTheme. Links in hidden content, such as a collapsed block, are skipped.
Links stay inline text, so they wrap and select as before. An invisible stand-in over each link takes the focus instead.
The default palette in MarkdownTheme.axaml meets WCAG 2.2 AA on FluentTheme's page colours (white with black text, and black with white text):
==marked== text, alert titles and alert bodies, <kbd> keys, the text of an image that could not be loaded, and body text, code and inline code on find highlights.<kbd> keys and of an image that could not be loaded, the image lightbox's close button, and a display formula's copy icon, which sits on the page rather than on the code block header.ThemeContrastTests checks every pair against the colours in the theme file, so a brush change that drops below these ratios fails the build.
Links use MarkdownLinkForeground, a shade of the OS accent colour rather than the accent itself:
SystemAccentColorDark2 in Light;SystemAccentColorLight2 in Dark.This follows WinUI, and keeps links above 4.5:1 for typical accent colours. Very light accents, such as gold or yellow, can still fall slightly short in Light. Links are always underlined, so they never rely on colour alone. These shades come from FluentTheme; with another base theme, define MarkdownLinkForeground yourself.
Avalonia reports whether the operating system asks for high contrast. It detects this on Windows, on macOS ("Increase contrast") and in the browser (prefers-contrast: more), but not on Linux. It ships neither a high-contrast theme nor the OS contrast colours, so MarkdownViewer brings its own palette. When high contrast is on, the viewer gets the :high-contrast pseudo-class, and MarkdownTheme.axaml then:
MarkdownHighContrastBackground / MarkdownHighContrastForeground;MarkdownHighContrastLink, and ==marked== text in MarkdownHighContrastHighlightForeground on MarkdownHighContrastHighlightBackground;MarkdownHighContrastBorder borders, and draws the code, its line numbers and the header's label and copy icon in MarkdownHighContrastForeground;MarkdownHighContrastForeground on MarkdownHighContrastBackground, with a MarkdownHighContrastBorder outline;It does this mostly by swapping values rather than through styles. The theme's MarkdownHighContrastPalette resource is a dictionary that gives a high-contrast value for each key that changes (MarkdownLinkForeground, MarkdownCodeBlockBackground, MarkdownBlockquoteOpacity…); a string entry names the MarkdownHighContrast* key the value comes from. While high contrast is on, the viewer adds it to its own Resources.MergedDictionaries, resolved for its theme variant, so every DynamicResource in the document picks up the new values; while it is off, nothing is added, and the mode costs nothing per element. Only the viewer itself and separators keep a :high-contrast style; ==marked== text and alert titles change through their control themes, which only cost on those controls.
| Resource key | Dark | Light |
|---|---|---|
MarkdownHighContrastBackground |
#000000 |
#FFFFFF |
MarkdownHighContrastForeground |
#FFFFFF |
#000000 |
MarkdownHighContrastLink |
#FFFF00 |
#00009F |
MarkdownHighContrastHighlightBackground |
#1AEBFF |
#37006E |
MarkdownHighContrastHighlightForeground |
#000000 |
#FFFFFF |
MarkdownHighContrastBorder |
#FFFFFF |
#000000 |
Every pair is at least 7:1, as Windows' contrast themes are, and ThemeContrastTests checks it. The GitHub preset redefines these keys with Primer's high-contrast colours; the Chat and Docs presets keep the default theme's. The column used follows the theme variant Avalonia reports along with the contrast setting. On Windows, Avalonia only reports Light for contrast themes whose name contains "White". Override any of these keys in your own resources to match your app's contrast palette; a preset changes its high-contrast look the same way, without styles of its own.
HighContrastMode controls when the palette is used:
| Value | Behaviour |
|---|---|
System (default) |
Follow the operating system, and update live when the user turns high contrast on or off. |
Enabled |
Always use the high-contrast palette, e.g. for an in-app "high contrast" toggle. |
Disabled |
Never use it. |
IsHighContrast reports whether the palette is currently in use. Style your own controls against the same pseudo-class:
<Style Selector="mv|MarkdownViewer:high-contrast TextBlock.my-caption">
<Setter Property="Opacity" Value="1" />
</Style>
Every control of a document in high contrast also inherits the MarkdownViewer.InHighContrast attached property (MarkdownViewer.GetInHighContrast(control)). A control theme can only select on the control itself, so it uses that property rather than the pseudo-class:
<ControlTheme x:Key="MyCaptionTheme" TargetType="TextBlock">
<Setter Property="Opacity" Value="0.8" />
<Style Selector="^[(mv|MarkdownViewer.InHighContrast)=True]">
<Setter Property="Opacity" Value="1" />
</Style>
</ControlTheme>
Syntax-highlighted code keeps its token colours in high contrast. The Dark+ and Light+ themes already contrast with black and white backgrounds. Mermaid diagrams keep their regular palette. Find highlights keep their colours too; the default palette's high-contrast text and links stay above 7:1 on them.