Accessibility
The library ships native accessibility support so screen readers (VoiceOver on iOS and TalkBack on Android) can navigate and announce Markdown content, with semantic labeling, custom navigation controls, and proper announcements for every supported element.
How content is announced
Plain paragraphs without inline links or images are announced as a single element per paragraph. Paragraphs containing links or images are split into text, link, and image parts so each stays independently navigable. List items follow the same logic. Whitespace-only segments are filtered out to avoid empty announcements.
Broadly, only actionable content (links, images) creates separate segments. Inline formatting - bold, italic, underline, strikethrough, inline code, spoiler - is deliberately not split into its own accessibility element; the paragraph is read as one element, matching how screen readers already ignore visual emphasis.
Translating announcements
The strings the screen reader speaks around your content - list announcements, blockquote suffixes, table rows, the math prefix, and the iOS rotor names - default to English. Each package overrides them its own way.
- React Native
- iOS
- Android
The strings the screen reader speaks around your content (list announcements, blockquote suffix, table rows, math prefix, and the iOS rotor names) are overridable through the React Native package's accessibilityLabels prop. Defaults are English; wire in your own i18n pipeline. Every field is optional - omit one and it keeps its English default.
Two strings are not covered: on Android the React Native package's "link" and "image" role descriptions are hardcoded English with no label key.
<EnrichedMarkdownText
markdown={markdown}
accessibilityLabels={{
list: {
bulletPoint: 'Punkt',
nestedBulletPoint: 'Eingebetteter Punkt',
orderedItem: 'Listenelement {n}',
nestedOrderedItem: 'Eingebettetes Listenelement {n}',
},
blockquote: { quote: 'Zitat', nestedQuote: 'Eingebettetes Zitat' },
table: { row: 'Zeile {n}: {content}' },
math: { equation: 'Formel: {latex}' },
rotor: { headings: 'Überschriften', links: 'Links', images: 'Bilder' },
}}
/>
The package also exports the defaults themselves - DEFAULT_ACCESSIBILITY_LABELS, the full English object, and resolveAccessibilityLabels(partial), which merges your overrides onto it. DEFAULT_ACCESSIBILITY_LABELS is the natural thing to seed a translation table from.
The standalone iOS SDK takes a MarkdownAccessibilityLabels value through .markdownAccessibilityLabels(_:). Every field defaults to English, so set only what you localize:
var labels = MarkdownAccessibilityLabels()
labels.list.top.bulletPoint = "Punkt"
labels.list.nested.bulletPoint = "Eingebetteter Punkt"
labels.list.top.orderedItem = "Listenelement {n}"
labels.list.top.checkedTask = "Aufgabe, erledigt"
labels.blockquote.quote = "Zitat"
labels.table.row = "Zeile {n}: {content}"
labels.image.fallback = "Bild"
labels.codeBlock.copy = "Code kopieren"
labels.rotor.headings = "Überschriften"
EnrichedMarkdownText(markdown)
.markdownAccessibilityLabels(labels)
Its shape differs from the React Native package's labels in a few places:
- Nesting is a
list.top/list.nestedpair, each withbulletPoint,orderedItem,checkedTaskanduncheckedTask, rather thannested…field names. image.fallback("Image") names an image that has no alt text, andcodeBlock.copy("Copy code") names the code-block copy action.- There is no
mathfield. The formula label is a parameter of.markdownLaTeX(accessibilityLabel:), which by default speaks an English rendering of the formula ("Math: {speech}"); use{latex}in the template to read the raw source instead.
The full defaults table is under .markdownAccessibilityLabels; the formula label is covered in LaTeX math.
Not localizable. The standalone Android SDK has no label API at any layer and ships no string resources: everything TalkBack hears from it - "bullet point", "list item {n}", "task, checked" / "task, not checked" and their "nested" variants, "heading level {n}", "Row {n}: {content}", "Math: {latex}", "Image", and the "link", "image" and "alert" role descriptions - is hard-coded English. See the roadmap.
Defaults
The React Native package's accessibilityLabels defaults. The standalone iOS SDK's are listed under .markdownAccessibilityLabels.
| Field | Default | Platform |
|---|---|---|
list.bulletPoint | "Bullet point" | iOS + Android |
list.nestedBulletPoint | "Nested bullet point" | iOS + Android |
list.orderedItem | "List item {n}" | iOS + Android |
list.nestedOrderedItem | "Nested list item {n}" | iOS + Android |
blockquote.quote | "Blockquote" | iOS + Android |
blockquote.nestedQuote | "Nested blockquote" | iOS + Android |
table.row | "Row {n}: {content}" | iOS + Android |
math.equation | "Math: {latex}" | iOS + Android |
codeBlock.wrap | "Wrap lines" | iOS + Android |
codeBlock.unwrap | "Disable line wrap" | iOS + Android |
rotor.headings / rotor.links / rotor.images | "Headings" / "Links" / "Images" | iOS only |
Placeholders: {n} (1-based index), {content} (comma-joined cell texts), {latex} (equation source). Preserve placeholder names exactly in translations. Defaults use the no-plural cardinal form so a single template works in every language.
Supported elements
What each element announces differs between the packages, so check the tab for the one you ship.
- React Native
- iOS
- Android
| Element | VoiceOver (iOS) | TalkBack (Android) |
|---|---|---|
| Headings | Rotor navigation, "heading" suffix | Reading-controls navigation, "heading" suffix |
| Links | Rotor navigation, activatable, "link" suffix | Reading-controls navigation, activatable, "link" suffix |
| Images | Alt text announced, rotor navigation, "image" suffix | Alt text announced, "image" role |
| List items | "Bullet point" / "List item N" added as the element's accessibility value | Same string set as the element's roleDescription, which replaces the role |
| Blockquotes | "Blockquote" / "Nested blockquote" added as the accessibility value | Same string set as roleDescription |
| Table rows | One focusable element per row, "Row N: <cells>"; header row carries the heading trait | One focusable overlay per row, same template; header row marked as heading |
| Math | "Math: <latex>" (LaTeX read verbatim) | Same |
A few element specifics in the React Native package:
- Headings announce the text followed by "heading". The level is dropped entirely - it is neither spoken nor exposed to platform navigation controls, so VoiceOver's rotor and TalkBack's reading controls jump between headings without distinguishing an H1 from an H4.
- Images with alt text announce it plus "image". An image without alt text behaves differently per platform: Android drops the element entirely, while iOS still creates a focusable, unlabeled element, so VoiceOver stops on it, says "image", and lists it in the Images rotor. Supply alt text in the Markdown either way.
- Math is read as raw LaTeX; the library does not convert it to natural language. Plug in a LaTeX-to-speech step on the consumer side if you need spoken math.
On iOS, VoiceOver also exposes custom rotors for headings, links, and images (a two-finger twist cycles rotors; swipe up/down jumps between elements of that type).
| Element | VoiceOver |
|---|---|
| Headings | Text plus "heading, level N" - the level is exposed through accessibilityTextHeadingLevel. Listed in the Headings rotor |
| Links | Announced as a link and activatable, listed in the Links rotor |
| Images | Alt text announced, listed in the Images rotor. An image with no alt text is announced as "Image" (image.fallback) |
| List items | "Bullet point" / "List item N", or "Nested …" inside another list |
| Task items | State announced: "Task, checked" / "Task, not checked" (and nested variants) |
| Blockquotes | "Blockquote" / "Nested blockquote" appended |
| Admonitions | The title is read as its own element first, then the body as a blockquote |
| Table rows | One element per row, "Row N: <cells>" |
| Code blocks | One element per block, with a "Copy code" custom action (codeBlock.copy) |
| Math | An English spoken form - "Math: x squared over 2" - rather than raw LaTeX |
VoiceOver also gets custom rotors for headings, links, and images (a two-finger twist cycles rotors; swipe up/down jumps between elements of that type). Every string above can be translated with .markdownAccessibilityLabels, and the math template with .markdownLaTeX(accessibilityLabel:).
| Element | TalkBack |
|---|---|
| Headings | "<text>, heading level N", marked as a heading for TalkBack's heading navigation |
| Links | Activatable, "link" role description |
| Images | Alt text with the "image" role description. An image with no alt text is announced as "Image" |
| List items | "bullet point" / "list item N", or "nested …" inside another list, as the role description |
| Task items | State announced: "task, checked" / "task, not checked" (and nested variants) |
| Blockquotes | No announcement - a quote is read as its text |
| Admonitions | The header is its own node with the role description "alert", announced ahead of the body |
| Table rows | One focusable element per row, "Row N: <cells>" |
| Math | A display-math block reads "Math: <latex>", the raw source. An inline formula is not read |
All of these strings are hard-coded English; see Translating announcements. Math is covered in LaTeX math.
Editor accessibility
The editor, EnrichedMarkdownTextInput, ships in the React Native package. Its model is intentionally simpler than the renderer's:
- iOS (VoiceOver): the field is a single accessibility element whose spoken value is the full plain text (delimiters stripped); when empty, the placeholder is announced.
accessibilityLabel/accessibilityHintare forwarded. Double-tap activates the field for editing. It is not announced with the native "text field" role, and has no in-field cursor navigation or per-character echo - exposing the inner text view would be unreliable under the custom TextKit stack. - Android (TalkBack): the input is a native
EditText, announced and edited as a standard editable field.
Font scaling
Text scaling is an accessibility setting too, and the React Native package's renderer honors it by default on iOS and Android: allowFontScaling is true, so every size in markdownStyle scales with the OS Text Size setting, and maxFontSizeMultiplier caps how far it goes (undefined or 0 means no cap).
Neither prop applies on web - both are stripped, and the web renderer emits absolute pixel sizes. A browser's minimum-font-size preference therefore has no effect on rendered Markdown; only page zoom does. If your web surface must respect a reader's text-size preference, drive markdownStyle sizes from your own CSS scale.
Accessibility on web
The React Native package's web renderer is semantic HTML, so screen readers get real structure with no configuration: <h1>-<h6>, <a>, <img alt>, <blockquote>, <ul>/<ol>, a full <table>, <pre><code>, and a real <input type="checkbox"> for task items. None of the native plumbing on this page applies - accessibilityLabels is stripped, the labels web emits are hard-coded English, and there is no rotor.
Four sharp edges are worth knowing:
- Setting
onCodeBlockPressoronImagePressputsrole="button"on the<pre>or<img>, which replaces their code and image semantics. - Table header cells have no
scopeattribute. - The horizontally scrollable table and code wrappers are not focusable and carry no
role="region", so a keyboard user cannot scroll them. role="math"witharia-label={latex}overrides the MathML KaTeX generates, so screen readers read the raw LaTeX rather than the structured equation.
Known limitations
These apply to the React Native package; the standalone SDKs' behavior is in the iOS and Android tabs under Supported elements.
- macOS screen-reader support is still pending (a no-op stub ships today); see macOS support.
- Android has no rotor concept, so
accessibilityLabels.rotor.*is ignored there. - Task list items have no accessibility handling in the React Native package on either platform: a checkbox is read as plain list text, with no checked state announced and no toggle action exposed. There is no label key for it.
- Admonitions carry no accessibility role; they are announced as the blockquote they are built from.
- Code blocks are covered only partially - under
flavor="github"the header copy button carries the resolved copy label and the optional wrap toggle (enableCodeBlockWrapToggle) speakscodeBlock.wrap/codeBlock.unwrap, while iOS vends both as VoiceOver actions, but the block itself has no dedicated role. - The platform tabs above describe the renderer. The editor speaks neither heading level nor task state (see Editor accessibility).
Reference
- React Native
- iOS
- Android
accessibilityLabels- translate every spoken string (see the Defaults table above).DEFAULT_ACCESSIBILITY_LABELSandresolveAccessibilityLabels()- the English defaults as a runtime value, and the merge helper, both exported from the package root.allowFontScaling/maxFontSizeMultiplier- see Font scaling above.
.markdownAccessibilityLabels- translate every spoken string, including the task states, the image fallback, the code-block copy action, and the rotor names..markdownLaTeX(accessibilityLabel:)- the formula label, as a{speech}/{latex}template or a closure.
- No label API - every spoken string is hard-coded English. See the roadmap.
- LaTeX math accessibility - what TalkBack reads for a formula.