Accessibility
editElement.ComputedRole and Element.ComputedName return the role and the accessible name that the accessibility tree of a browser exposes for an element: the values that the WebDriver commands Get Computed Role and Get Computed Label return. Use them to check that generated HTML is accessible:
using var context = new BrowsingContext();
var document = context.Navigate("https://example.com/", _ => """
<label>Email <input type="email" id="email"></label>
<button id="close" aria-label="Close"><svg></svg></button>
<img id="logo" src="logo.png">
""");
// The role and the accessible name that an accessibility tree exposes, as WebDriver reports them
var email = document.GetElementById("email")!;
Assert.Equal(("textbox", "Email"), (email.ComputedRole, email.ComputedName));
var close = document.GetElementById("close")!;
Assert.Equal(("button", "Close"), (close.ComputedRole, close.ComputedName));
// An image without alt attribute has no name: a check of generated HTML can report it
var logo = document.GetElementById("logo")!;
Assert.Equal(("image", ""), (logo.ComputedRole, logo.ComputedName));Role
The role is the first valid role of the role attribute (img is reported as image, presentation as none), else the role given by ElementInternals for a custom element, else the implicit role of the element: heading, link for an a with an href, listitem, textbox, combobox, generic for a div or a span... An empty string means that the element has no role (head, br).
An element that is not rendered (display: none on it or on an ancestor) is not in the accessibility tree: its role is none. The elements hidden by visibility: hidden or aria-hidden="true" keep their role.
Accessible name
The name comes, in order, from aria-labelledby, aria-label, the native text alternatives (the associated label, alt, the value of a button, placeholder, caption, legend, figcaption, the SVG title), the content of the element for the roles that take their name from it (buttons, links, headings...), then title. The whitespace is collapsed and trimmed. The hidden nodes are not part of a name, unless aria-labelledby references them.
Properties and states
Element.GetAccessibilityProperties returns everything the accessibility tree exposes for an element, computed when you call it:
RoleandName: the values ofComputedRoleandComputedName.Description: the text of the elements referenced byaria-describedby, elsearia-description, else thetitleattribute when it is not the name.CheckedandPressed:True,FalseorMixed, ornullwhen the role has no such state. A checkbox withoutaria-checkedis not checked; an indeterminate checkbox isMixed. A button is a toggle button only when it hasaria-pressed.ExpandedandSelected:aria-expandedandaria-selected, or the selectedness of anoption.Disabled: a disabled form control (including by a disabledfieldset,optgrouporselect), oraria-disabled="true"on the element or an ancestor.Level: the level of a heading, a list item, a row or a tree item. Forh1toh6, it is the computed heading level, which adds theheadingoffsetattributes of the ancestors up to aheadingreset: anh1in<section headingoffset="1">has level 2.aria-leveltakes precedence, and an element withrole="heading"has level 2 by default.IsHidden: whether the element is excluded from the tree (display: none,visibility: hidden, oraria-hidden="true"on it or an ancestor).
Accessibility tree
Element.CaptureAccessibilityTree captures the tree rooted at an element. The result is a snapshot: change the document, and capture the tree again to see the change. Each AccessibilityNode has its DOM Node, its Properties, its Parent and its Children. ToAriaSnapshot writes the tree in the YAML format of the aria snapshots of Playwright, so you can compare it with an expected text:
using var context = new BrowsingContext();
var document = context.Navigate("https://example.com/", _ => """
<main>
<section headingoffset="1">
<h1>Sign in</h1>
<label>Email <input type="email" placeholder="you@example.com"></label>
<label><input type="checkbox" checked> Remember me</label>
<button disabled>Continue</button>
</section>
</main>
""");
// The properties of one element
var button = document.QuerySelector("button")!.GetAccessibilityProperties();
Assert.Equal(("button", "Continue", true), (button.Role, button.Name, button.Disabled));
// The level of a heading takes headingoffset into account
var heading = document.QuerySelector("h1")!.GetAccessibilityProperties();
Assert.Equal(2, heading.Level);
// A snapshot of the tree, in the format of the aria snapshots of Playwright
var tree = document.Body!.CaptureAccessibilityTree()!;
Assert.Equal("""
- main:
- heading "Sign in" [level=2]
- text: Email
- textbox "Email":
- /placeholder: you@example.com
- checkbox "Remember me" [checked]
- text: Remember me
- button "Continue" [disabled]
""", tree.ToAriaSnapshot());The tree is built as Playwright builds its snapshots:
- A hidden element is excluded with its whole subtree.
CaptureAccessibilityTreereturnsnullfor a hidden element. - The elements without a role (
label,br...), the presentational elements and the generic elements (div,span...) are not nodes: their content belongs to their parent. - The adjacent texts are merged into one
textnode, with the white space collapsed. A text that repeats the name of its parent is dropped (the text of a button named by its content). A text field shows its value. - The children follow the flat tree (the shadow trees and the slotted nodes), with the relocations of
aria-owns.
The snapshot uses the names and the states of GetAccessibilityProperties, so it differs from the snapshot of Playwright where the specifications and Playwright disagree:
- The rows and the cells of a table are not named by their content (Playwright writes
row "Cell 1 Cell 2"). - An
article, adialogand analertdialogare named by their first heading. - A disabled
fieldsetis disabled itself, not only its form controls. - The level of a heading takes
headingoffsetinto account.
With and without a browsing context
The computation uses the computed styles: the generated content of ::before and ::after, display: none and visibility: hidden. Load the document in a browsing context, as above, to get the names a browser gives. In a document parsed by HtmlDocumentParser, only the hidden and aria-hidden attributes hide the nodes, and there is no generated content.