agentleFS
IceMasterTIceMasterT/Unity-6.3_SKILLS/unity-accessibility/SKILL.md

unity-accessibility

Skill0 starsChanged 3 months agoMITView on GitHub

Use when implementing accessibility features in a Unity project — screen reader support, the Accessibility Hierarchy, or related APIs. Grounds answers in the local Unity 6.3 docs over pretrained knowledge.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All 90 Accessibility.* ScriptReference pages plus the 7 Manual accessibility pages and the 2 PlayerLoop insertion-point pages above were …

    line 58
  2. Distinctive:

    Only the hierarchy attached to the application's main window is exposed to screen readers.

    line 365

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Unity Accessibility

Retrieval Sources

SourcePathUse for
Accessibility module overviewManual/com.unity.modules.accessibility.html, Manual/accessibility/_index.html, Manual/accessibility/module-intro.htmlWhat the Accessibility module covers, supported platforms, how to enable it via Package Manager's Built-in section
Module architecture & performanceManual/accessibility/module-architecture.htmlThe six core components (AssistiveSupport, AccessibilityHierarchy, AccessibilityNode, IAccessibilityNotificationDispatcher, AccessibilitySettings, VisionUtility), module lifecycle stages (start/Update/LateUpdate/quit), and performance best practices
Accessibility concepts indexManual/accessibility/accessibility-concepts/_index.htmlLanding page linking screen-reader and fundamentals concept pages
Screen reader fundamentalsManual/accessibility/accessibility-concepts/fundamentals.htmlBaseline concepts: what accessibility means for Unity apps, why native UI systems aren't accessible by default
Screen reader introManual/accessibility/accessibility-concepts/screen-readers-intro.htmlScreen reader traversal model, roles/labels/values concepts, gesture-vs-announcement model
Screen reader get-started walkthroughManual/accessibility/screen-readers-get-started.htmlFull worked example: UI Toolkit button → AccessibilityHierarchy → AccessibilityNode → frame tracking → invoked event wiring; also documents the built-in Window > Accessibility > Hierarchy Viewer debug tool
Accessibility module entry point (ScriptReference)ScriptReference/UnityEngine.AccessibilityModule.htmlModule-level index of every public type in UnityEngine.Accessibility
AccessibilityHierarchy classScriptReference/Accessibility.AccessibilityHierarchy.html, Accessibility.AccessibilityHierarchy-ctor.htmlClass description: hierarchy operates independently of the UI hierarchy; empty-constructor signature public AccessibilityHierarchy()
AccessibilityHierarchy — adding nodesAccessibility.AccessibilityHierarchy.AddNode.html, Accessibility.AccessibilityHierarchy.InsertNode.htmlAddNode(string label, AccessibilityNode parent) appends; InsertNode(int childIndex, string label, AccessibilityNode parent) places at a specific position
AccessibilityHierarchy — removing/clearingAccessibility.AccessibilityHierarchy.RemoveNode.html, Accessibility.AccessibilityHierarchy.Clear.htmlRemoving a single node (and its subtree) vs. resetting the whole hierarchy to empty (also clears screen reader focus)
AccessibilityHierarchy — moving nodesAccessibility.AccessibilityHierarchy.MoveNode.htmlMoveNode(AccessibilityNode node, AccessibilityNode newParent, int newChildIndex); documents a non-trivial per-platform cost (high on iOS, moderate on macOS) when called on the active hierarchy
AccessibilityHierarchy — lookupsAccessibility.AccessibilityHierarchy.TryGetNode.html, Accessibility.AccessibilityHierarchy.TryGetNodeAt.html, Accessibility.AccessibilityHierarchy.ContainsNode.htmlTryGetNode(int id, out AccessibilityNode node) by ID; TryGetNodeAt by hierarchy position; ContainsNode membership check
AccessibilityHierarchy — tree queriesAccessibility.AccessibilityHierarchy.GetLowestCommonAncestor.htmlGetLowestCommonAncestor(AccessibilityNode firstNode, AccessibilityNode secondNode) — returns null if the two nodes share no ancestor
AccessibilityHierarchy — frame refreshAccessibility.AccessibilityHierarchy.RefreshNodeFrames.htmlBulk-updates every node's frame from its frameGetter and calls SendLayoutChanged(null); use on scroll/orientation-change, not per-frame
AccessibilityHierarchy — root nodesAccessibility.AccessibilityHierarchy-rootNodes.htmlrootNodes property — top-level nodes with no parent
AccessibilityNode classScriptReference/Accessibility.AccessibilityNode.htmlFull class description: nodes are independent data structures, not tied to visual element lifecycle; lists supported RuntimePlatforms
AccessibilityNode — identity & tree fieldsAccessibility.AccessibilityNode-id.html, Accessibility.AccessibilityNode-parent.html, Accessibility.AccessibilityNode-children.html, Accessibility.AccessibilityNode-isActive.htmlUnique ID; parent/children tree links; isActive (default true) toggles screen-reader visibility without removing the node
AccessibilityNode — semantic contentAccessibility.AccessibilityNode-label.html, Accessibility.AccessibilityNode-value.html, Accessibility.AccessibilityNode-hint.html, Accessibility.AccessibilityNode-role.html, Accessibility.AccessibilityNode-state.htmlThe five fields a screen reader actually announces: what it is, its current value, extra guidance, its control type, its status
AccessibilityNode — screen geometryAccessibility.AccessibilityNode-frame.html, Accessibility.AccessibilityNode-frameGetter.htmlframe (Rect in screen coordinates) can be set directly or computed lazily via frameGetter (a Func<Rect>), which also backstops RefreshNodeFrames
AccessibilityNode — focusAccessibility.AccessibilityNode-isFocused.html, Accessibility.AccessibilityNode-focusChanged.htmlWhether the screen reader is currently on this node, and the main-thread event fired when that changes
AccessibilityNode — direct interactionAccessibility.AccessibilityNode-allowsDirectInteraction.htmlWhether raw touch passes through to the element instead of being intercepted by screen-reader gestures (e.g., for custom drawing surfaces)
AccessibilityNode — interaction eventsAccessibility.AccessibilityNode-invoked.html, Accessibility.AccessibilityNode-dismissed.html, Accessibility.AccessibilityNode-scrolled.htmlFired on the screen reader's "activate", "dismiss", and "scroll" gestures respectively
AccessibilityNode — increment/decrement eventsAccessibility.AccessibilityNode-incremented.html, Accessibility.AccessibilityNode-decremented.htmlFired on "increment"/"decrement" gestures (e.g., adjusting a slider without dragging)
AccessibilityNode — deprecated eventAccessibility.AccessibilityNode-selected.htmlselected is deprecated — use invoked instead (confirmed in current docs)
AccessibilityNode — utility overridesAccessibility.AccessibilityNode.GetHashCode.html, Accessibility.AccessibilityNode.ToString.htmlStandard hash/debug-string overrides
AccessibilityRole enumAccessibility.AccessibilityRole.htmlEnum overview + a full worked C# example assigning roles per VisualElement type and building custom UI Toolkit controls for roles with no built-in element
AccessibilityRole valuesAccessibility.AccessibilityRole.Button.html, .Container.html, .Dropdown.html, .Header.html, .Image.html, .KeyboardKey.html, .None.html, .ScrollView.html, .SearchField.html, .Slider.html, .StaticText.html, .TabBar.html, .TabButton.html, .TextField.html, .Toggle.htmlEvery concrete role value; None is the explicit fallback when no role fits — combine with a strong label/hint instead
AccessibilityState enumAccessibility.AccessibilityState.htmlBit-flag-style enum for a node's current status (e.g., checked/disabled); None is the fallback, paired with value/hint for detail
AccessibilityState valuesAccessibility.AccessibilityState.None.html, .Disabled.html, .Selected.html, .Expanded.htmlConcrete state flags surfaced to screen readers
AccessibilityScrollDirection enumAccessibility.AccessibilityScrollDirection.htmlDirection reported to/via the scrolled event and SendPageScrolledAnnouncement
AccessibilityScrollDirection values.Unknown.html, .Up.html, .Down.html, .Left.html, .Right.html, .Forward.html, .Backward.htmlAll seven direction values, including axis-relative Forward/Backward for non-2D scroll contexts
AssistiveSupport classScriptReference/Accessibility.AssistiveSupport.htmlEntry point class description; documents supported RuntimePlatforms (Android 8+, iOS, macOS, Windows) and that platform behavior may differ slightly but stays consistent with native UI conventions
AssistiveSupport — screen reader statusAccessibility.AssistiveSupport-isScreenReaderEnabled.html, Accessibility.AssistiveSupport-screenReaderStatusChanged.htmlisScreenReaderEnabled (static bool) for polling; screenReaderStatusChanged (main-thread event) for reacting live
AssistiveSupport — active hierarchyAccessibility.AssistiveSupport-activeHierarchy.htmlactiveHierarchy — assigning it publishes the tree (and calls SendScreenChanged(null)); auto-reset to null when the screen reader turns off; non-trivial cost on iOS/macOS to build/tear down
AssistiveSupport — focus eventAccessibility.AssistiveSupport-nodeFocusChanged.htmlFires when the user moves screen-reader focus to a different node anywhere in the active hierarchy
AssistiveSupport — notification dispatcherAccessibility.AssistiveSupport-notificationDispatcher.htmlExposes the IAccessibilityNotificationDispatcher singleton used to push change notifications to the screen reader
AssistiveSupport — status overrideAccessibility.AssistiveSupport-screenReaderStatusOverride.html, Accessibility.AssistiveSupport.ScreenReaderStatusOverride.html, .OSDriven.html, .ForceEnabled.html, .ForceDisabled.htmlForces isScreenReaderEnabled to a fixed value for Editor/Player testing without a real assistive device; OSDriven is the default
IAccessibilityNotificationDispatcher interfaceScriptReference/Accessibility.IAccessibilityNotificationDispatcher.htmlImplemented by AssistiveSupport.notificationDispatcher; warns that rapid repeated notifications of the same type may be coalesced/dropped by the screen reader
IAccessibilityNotificationDispatcher methods.SendAnnouncement.html, .SendLayoutChanged.html, .SendScreenChanged.html, .SendPageScrolledAnnouncement.htmlSendAnnouncement(string) for ambient one-off messages (no effect from an iOS button callback); SendLayoutChanged/SendScreenChanged for local vs. large-scale UI changes; SendPageScrolledAnnouncement for scroll completion
AccessibilitySettings classScriptReference/Accessibility.AccessibilitySettings.htmlRead-only OS accessibility preference mirror; supported on Android and iOS only (not macOS/Windows)
AccessibilitySettings — font scaleAccessibility.AccessibilitySettings-fontScale.html, Accessibility.AccessibilitySettings-fontScaleChanged.htmlUser's OS text-size multiplier and its change event
AccessibilitySettings — bold textAccessibility.AccessibilitySettings-isBoldTextEnabled.html, Accessibility.AccessibilitySettings-boldTextStatusChanged.htmlOS bold-text preference and its change event
AccessibilitySettings — captionsAccessibility.AccessibilitySettings-isClosedCaptioningEnabled.html, Accessibility.AccessibilitySettings-closedCaptioningStatusChanged.htmlOS closed-captioning preference and its change event
VisionUtility classScriptReference/Accessibility.VisionUtility.html, Accessibility.VisionUtility.GetColorBlindSafePalette.htmlGetColorBlindSafePalette returns a palette distinguishable under normal vision, deuteranopia, protanopia, and tritanopia — general visual-accessibility helper independent of the screen-reader APIs
Player loop insertion pointsScriptReference/PlayerLoop.PreLateUpdate.AccessibilityUpdate.html, ScriptReference/PlayerLoop.PostLateUpdate.AccessibilityLateUpdate.htmlConfirms the module-architecture doc's lifecycle claim: accessibility event batching runs in PreLateUpdate, automatic frame refresh runs in PostLateUpdate

All 90 Accessibility.* ScriptReference pages plus the 7 Manual accessibility pages and the 2 PlayerLoop insertion-point pages above were re-verified on disk under /media/artiq/FRESH_DRIVE/Books/Unity6/Documentation/en/ as part of this pass; nothing in this table is inferred from memory alone.

Key Guidelines

AccessibilityHierarchy & Node Trees

The AccessibilityHierarchy is a separate data model that runs in parallel to your scene/UI hierarchy, not a reflection of it — screen readers cannot see GameObjects, VisualElements, or RectTransforms directly, only the tree of AccessibilityNodes you build and publish. A node's position in this tree (its parent/children, and its rank among rootNodes) determines screen-reader focus order and grouping; some platforms traverse depth-first through children before siblings, others use on-screen position (frame). Because nested structures are harder to navigate by voice, the module-architecture doc explicitly recommends flattening the hierarchy where possible. Build the hierarchy with AddNode/InsertNode, prune it with RemoveNode/Clear, and reparent/reorder with MoveNode — but treat MoveNode and reassigning AssistiveSupport.activeHierarchy as expensive operations on iOS/macOS when done against the active hierarchy, since Unity must rebuild native accessibility objects; batch structural changes rather than performing them one at a time on the live tree.

using UnityEngine;
using UnityEngine.Accessibility;

public class MenuAccessibilityBuilder : MonoBehaviour
{
    AccessibilityHierarchy m_Hierarchy;

    void BuildMenuHierarchy()
    {
        m_Hierarchy = new AccessibilityHierarchy();

        // Root container groups the whole panel.
        AccessibilityNode panelNode = m_Hierarchy.AddNode("Main Menu", parent: null);
        panelNode.role = AccessibilityRole.Container;

        // Child nodes under the panel — order here drives read order.
        AccessibilityNode startNode = m_Hierarchy.AddNode("Start Game", panelNode);
        startNode.role = AccessibilityRole.Button;

        AccessibilityNode settingsNode = m_Hierarchy.AddNode("Settings", panelNode);
        settingsNode.role = AccessibilityRole.Button;

        // Insert a node at a specific position instead of appending.
        AccessibilityNode quitNode = m_Hierarchy.InsertNode(0, "Quit", panelNode);
        quitNode.role = AccessibilityRole.Button;

        // Find the lowest common ancestor of two nodes, e.g. for focus management.
        AccessibilityNode commonAncestor =
            m_Hierarchy.GetLowestCommonAncestor(startNode, settingsNode); // panelNode
    }
}

AssistiveSupport & Screen Reader Detection

AssistiveSupport is the single entry point that connects your hierarchy to the OS: check AssistiveSupport.isScreenReaderEnabled to know whether assistive tech is active right now, and subscribe to AssistiveSupport.screenReaderStatusChanged to react the moment the user turns it on or off — build/refresh and assign your hierarchy in that handler rather than eagerly at startup, since Unity discards the active hierarchy whenever the screen reader is off (to save resources) and resets activeHierarchy to null automatically. Setting AssistiveSupport.activeHierarchy only takes effect while isScreenReaderEnabled is true; you must re-set it every time the screen reader is (re-)enabled, not just once. Use AssistiveSupport.nodeFocusChanged if you need to know which node the screen reader is currently on (e.g., to sync a visual focus highlight).

using UnityEngine;
using UnityEngine.Accessibility;

public class ScreenReaderGate : MonoBehaviour
{
    AccessibilityHierarchy m_Hierarchy;

    void OnEnable()
    {
        AssistiveSupport.screenReaderStatusChanged += OnScreenReaderStatusChanged;
        // Handle the case where a screen reader is already on when this object enables.
        if (AssistiveSupport.isScreenReaderEnabled)
            OnScreenReaderStatusChanged(true);
    }

    void OnDisable()
    {
        AssistiveSupport.screenReaderStatusChanged -= OnScreenReaderStatusChanged;
    }

    void OnScreenReaderStatusChanged(bool enabled)
    {
        if (enabled)
        {
            m_Hierarchy ??= BuildHierarchy();
            AssistiveSupport.activeHierarchy = m_Hierarchy; // publish it
        }
        // No need to explicitly clear activeHierarchy on disable — Unity does it.
    }

    AccessibilityHierarchy BuildHierarchy() => new AccessibilityHierarchy();
}

Labeling & Roles for Screen Readers

Every node needs a meaningful label (what the element is) and, where relevant, a value (its current state, such as a slider's number or a toggle's on/off), a hint (what happens if the user activates it), a role (AccessibilityRole.Button, .Toggle, .Header, .Slider, .StaticText, .TextField, .SearchField, .Dropdown, .TabBar/.TabButton, .KeyboardKey, .ScrollView, .Container, .Image, …), and a state (AccessibilityState.Disabled, .Selected, .Expanded, or .None). Generic or empty labels ("Button", "Image", "") are the single most common real-world screen-reader complaint — they leave the user with no idea what the control does. If nothing in AccessibilityRole fits, fall back to AccessibilityRole.None and put the missing semantic information into label/hint instead of leaving the role unset carelessly. Respond to the node's invoked (activate), incremented/decremented (steppers/sliders), dismissed, and scrolled events — do not use the deprecated selected event, which the docs explicitly say to replace with invoked.

using UnityEngine.Accessibility;

void ConfigureVolumeSlider(AccessibilityHierarchy hierarchy, AccessibilityNode parent, int currentVolume)
{
    AccessibilityNode sliderNode = hierarchy.AddNode("Master Volume", parent);
    sliderNode.role = AccessibilityRole.Slider;
    sliderNode.value = $"{currentVolume}%";
    sliderNode.hint = "Adjusts the overall game volume";
    sliderNode.state = currentVolume == 0 ? AccessibilityState.None : AccessibilityState.None;

    sliderNode.incremented += () => SetVolume(currentVolume + 10);
    sliderNode.decremented += () => SetVolume(currentVolume - 10);
    sliderNode.invoked    += () => ToggleMute();
}

Integrating with UI Toolkit/uGUI Widgets

The screen-reader APIs are UI-framework-agnostic — they work identically with UI Toolkit, uGUI, a custom framework, or non-UI 2D/3D world objects — because nodes are independent of any visual element's lifecycle. That independence cuts both ways: Unity does not auto-populate nodes for your widgets, and a node's frame/label/state will silently drift out of sync with the widget it represents unless you explicitly keep them updated. The standard pattern (per the official get-started walkthrough) is: create one AccessibilityNode per interactive/informative visual element, track the widget's GeometryChangedEvent (UI Toolkit) or its RectTransform/layout changes (uGUI) to recompute frame in screen coordinates (worldBound.position * scaledPixelsPerPoint, or equivalent), and wire the node's invoked event to the same code path the widget's own click/submit handler uses. For elements with a role that has no matching built-in VisualElement (custom controls used in UXML/UI Builder), the AccessibilityRole reference page has a worked example of authoring a custom control with the correct role assigned directly.

using UnityEngine;
using UnityEngine.Accessibility;
using UnityEngine.UIElements;

public class AccessibleStartMenu : MonoBehaviour
{
    Button m_Button;
    AccessibilityHierarchy m_AccessibilityHierarchy;
    AccessibilityNode m_AccessibilityNode;

    void OnEnable()
    {
        VisualElement root = GetComponent<UIDocument>().rootVisualElement;
        m_Button = root.Q<Button>("startButton");
        m_Button.clicked += OnButtonClicked;
        m_Button.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);

        CreateAccessibilityHierarchy();
    }

    void OnDisable()
    {
        m_Button.clicked -= OnButtonClicked;
        m_Button.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
    }

    void CreateAccessibilityHierarchy()
    {
        m_AccessibilityHierarchy = new AccessibilityHierarchy();
        m_AccessibilityNode = m_AccessibilityHierarchy.AddNode(m_Button.text);
        m_AccessibilityNode.role = AccessibilityRole.Button;
        m_AccessibilityNode.state = m_Button.enabledSelf
            ? AccessibilityState.None
            : AccessibilityState.Disabled;
        // Route the screen reader's "activate" gesture to the same handler as a real tap.
        m_AccessibilityNode.invoked += () =>
            m_Button.SendEvent(NavigationSubmitEvent.GetPooled());
    }

    void OnGeometryChanged(GeometryChangedEvent evt)
    {
        Rect worldRect = m_Button.worldBound;
        float scale = m_Button.panel.scaledPixelsPerPoint;
        m_AccessibilityNode.frame = new Rect(worldRect.position * scale, worldRect.size * scale);
    }

    void OnButtonClicked() => Debug.Log("Start Game button clicked");
}

Testing with Platform Screen Readers

This is a runtime-platform feature — it drives the actual OS screen reader (VoiceOver on iOS/macOS, TalkBack on Android, Narrator on Windows) inside a built Player — and is distinct from Unity Editor UX/accessibility. Use AssistiveSupport.screenReaderStatusOverride (values OSDriven, ForceEnabled, ForceDisabled) to force isScreenReaderEnabled to a known value in the Editor or a development Player when no real assistive device is attached, so your screenReaderStatusChanged handler and hierarchy-build path can be exercised without a physical accessibility session. That said, override testing is a smoke test, not a substitute for the real thing: gesture sets differ per platform (see Advanced Notes), and only an on-device VoiceOver/TalkBack/Narrator session exercises the actual native traversal, announcement timing, and gesture mapping. Unity also ships a built-in inspector for this: Window > Accessibility > Hierarchy Viewer lets you inspect live AccessibilityNode properties (label, role, state, frame) in Play Mode without leaving the Editor, which is the fastest way to catch structural mistakes before an on-device pass.

using UnityEngine.Accessibility;

#if UNITY_EDITOR
public static class AccessibilityTestUtility
{
    public static void ForceScreenReaderForTesting(bool enabled)
    {
        AssistiveSupport.screenReaderStatusOverride = enabled
            ? AssistiveSupport.ScreenReaderStatusOverride.ForceEnabled
            : AssistiveSupport.ScreenReaderStatusOverride.ForceDisabled;
    }

    public static void RestoreOSDrivenStatus()
    {
        AssistiveSupport.screenReaderStatusOverride =
            AssistiveSupport.ScreenReaderStatusOverride.OSDriven;
    }
}
#endif

Broader Accessibility Practices (beyond the API)

Screen-reader support is only one axis of accessibility. AccessibilitySettings mirrors OS-level preferences — fontScale, isBoldTextEnabled, isClosedCaptioningEnabled (Android and iOS only; unsupported on macOS/Windows Player) — so read these at startup and react to their *Changed events instead of hardcoding fixed font sizes or ignoring OS caption preferences. VisionUtility.GetColorBlindSafePalette returns a palette distinguishable under normal vision, deuteranopia, protanopia, and tritanopia — use it for any UI or gameplay signal (team colors, status indicators, minimap markers) that currently relies on hue alone, and pair it with contrast-conscious design and non-color redundant cues (icons, patterns, text). Beyond what any Unity API covers directly: ensure adequate color contrast ratios, make font/UI scale respect fontScale rather than fixed pixel sizes, and provide input remapping / alternative control schemes for players who cannot use the default scheme — these matter for players who are not screen-reader users at all, and a project can pass every screen-reader checklist item while still being inaccessible on these other axes.

using UnityEngine;
using UnityEngine.Accessibility;

public class AccessibilityPreferencesSync : MonoBehaviour
{
    void OnEnable()
    {
        ApplyFontScale(AccessibilitySettings.fontScale);
        AccessibilitySettings.fontScaleChanged += ApplyFontScale;
        AccessibilitySettings.closedCaptioningStatusChanged += ApplyCaptionPreference;
    }

    void OnDisable()
    {
        AccessibilitySettings.fontScaleChanged -= ApplyFontScale;
        AccessibilitySettings.closedCaptioningStatusChanged -= ApplyCaptionPreference;
    }

    void ApplyFontScale(float scale) { /* scale UI Toolkit root's font-size or uGUI CanvasScaler */ }
    void ApplyCaptionPreference(bool enabled) { /* show/hide subtitle track */ }

    Color[] GetTeamColors(int count) =>
        VisionUtility.GetColorBlindSafePalette(count, minHueDistance: 0f, minSaturationDistance: 0f);
}

Common Mistakes

MistakeWhy it happens / fix
Custom UI Toolkit/uGUI widgets never registered in the hierarchyUnity doesn't auto-populate nodes for hand-rolled widgets; explicitly create and add an AccessibilityNode for each custom control
Nodes with empty or generic labels/rolesCopy-pasted node setup without per-element text; screen readers announce nothing useful — always set label/role/value per node
Never testing with a real platform screen readerEditor-only testing hides real announcement/order bugs; use screenReaderStatusOverride in-editor, then verify on-device with VoiceOver/TalkBack/Narrator
Hierarchy built once at startup and never refreshedDynamic UI (menus opening/closing, lists scrolling) drifts from the tree; rebuild/refresh affected nodes when layout changes, and call RefreshNodeFrames after bulk layout shifts
Treating accessibility as screen-reader-onlyIgnoring contrast, font scaling, and input remapping leaves non-screen-reader accessibility needs unaddressed even when the API checklist is "done"
Rebuilding/reassigning activeHierarchy every frame or on every minor changeactiveHierarchy assignment and MoveNode on the active hierarchy have real native cost (high on iOS, moderate on macOS); batch structural changes instead of doing them continuously
Using the deprecated selected eventDocs mark AccessibilityNode.selected deprecated in favor of invoked; new code should never wire up selected
Assuming activeHierarchy stays set once assignedUnity automatically resets activeHierarchy to null whenever the screen reader turns off, to save resources; you must re-assign it every time screenReaderStatusChanged fires true, not just once at app start
Never calling SendLayoutChanged/SendScreenChanged after structural editsModifying the active hierarchy's structure without notifying the dispatcher leaves the screen reader's internal state stale; use SendLayoutChanged for local changes and SendScreenChanged for large-scale ones
Spamming SendAnnouncement or other notifications every frameThe dispatcher docs warn that sending the same notification type in quick succession can cause the screen reader to skip some of them; send notifications only when something meaningfully changed
Calling SendAnnouncement from an iOS button callback and expecting it to workDocumented iOS-specific no-op case — announcements triggered from a button's own callback have no effect on iOS; trigger them from elsewhere in the flow
Forgetting frame is in screen coordinates, not local/world coordinatesNode frames must be computed as worldBound.position * scaledPixelsPerPoint (or the uGUI equivalent) — using raw local rect or world-space coordinates misplaces the on-screen hit target for position-based screen-reader navigation
Reading AccessibilitySettings properties on macOS/Windows and expecting real valuesAccessibilitySettings (fontScale, bold text, captions) is documented as Android/iOS only; unlike AssistiveSupport, it has no macOS/Windows support
Deeply nested accessibility hierarchies mirroring deep GameObject hierarchiesThe architecture doc explicitly recommends flattening — deep nesting makes screen-reader navigation slow and confusing even if the visual UI is shallow
Creating nodes for every visual element indiscriminatelyPerformance guidance recommends nodes only for elements that are interactive/informative and currently visible on screen; hierarchy size and update frequency both affect performance while a screen reader is active

Quick Reference

ItemPurpose
AccessibilityHierarchyTree of nodes exposed to the platform screen reader; independent of the visual UI hierarchy
AccessibilityHierarchy() (ctor)Creates an empty hierarchy
AccessibilityHierarchy.AddNode(label, parent)Creates and appends a node under parent (or at root if null)
AccessibilityHierarchy.InsertNode(childIndex, label, parent)Creates and inserts a node at a specific position
AccessibilityHierarchy.RemoveNodeRemoves a node (and its subtree)
AccessibilityHierarchy.ClearResets hierarchy to empty, clears screen-reader focus
AccessibilityHierarchy.MoveNode(node, newParent, newChildIndex)Reparents/reorders a node; costly on iOS/macOS when active
AccessibilityHierarchy.TryGetNode(id, out node)Look up a node by ID
AccessibilityHierarchy.TryGetNodeAtLook up a node by hierarchy position
AccessibilityHierarchy.ContainsNodeMembership check
AccessibilityHierarchy.GetLowestCommonAncestor(a, b)Finds shared ancestor of two nodes
AccessibilityHierarchy.RefreshNodeFrames()Bulk-refreshes all node frames from frameGetter, notifies via SendLayoutChanged
AccessibilityHierarchy.rootNodesTop-level nodes with no parent
AccessibilityNodeSingle element: label, value, hint, role, state, frame, focus/invoke events
AccessibilityNode.idUnique node identifier
AccessibilityNode.parent / .childrenTree links
AccessibilityNode.isActiveWhether the node is currently exposed to screen readers (default true)
AccessibilityNode.labelShort description announced by the screen reader
AccessibilityNode.valueCurrent value/state text (e.g. slider number)
AccessibilityNode.hintExtra guidance on what interacting does
AccessibilityNode.roleControl-type semantic (AccessibilityRole)
AccessibilityNode.stateStatus flags (AccessibilityState)
AccessibilityNode.frameBounding rect in screen coordinates
AccessibilityNode.frameGetterFunc<Rect> that computes frame lazily/automatically
AccessibilityNode.isFocused / .focusChangedCurrent screen-reader focus state and its change event
AccessibilityNode.allowsDirectInteractionLets raw touch bypass screen-reader gesture interception
AccessibilityNode.invokedFired on screen-reader "activate" gesture
AccessibilityNode.dismissedFired on "dismiss" gesture
AccessibilityNode.scrolledFired on "scroll" gesture
AccessibilityNode.incremented / .decrementedFired on "increment"/"decrement" gestures
AccessibilityNode.selectedDeprecated — use invoked
AccessibilityRole (enum)Button, Toggle, Header, Slider, StaticText, TextField, SearchField, Dropdown, Container, Image, KeyboardKey, ScrollView, TabBar, TabButton, None
AccessibilityState (enum)None, Disabled, Selected, Expanded
AccessibilityScrollDirection (enum)Unknown, Up, Down, Left, Right, Forward, Backward
AssistiveSupportEntry point — screen reader status, active hierarchy, status/focus-changed events, notification dispatcher
AssistiveSupport.isScreenReaderEnabledStatic bool — is a screen reader on right now
AssistiveSupport.screenReaderStatusChangedEvent fired when the OS screen reader is toggled
AssistiveSupport.activeHierarchyThe hierarchy currently published to the screen reader
AssistiveSupport.nodeFocusChangedEvent fired when screen-reader focus moves to a different node
AssistiveSupport.notificationDispatcherThe IAccessibilityNotificationDispatcher instance
AssistiveSupport.screenReaderStatusOverrideForces isScreenReaderEnabled for testing
AssistiveSupport.ScreenReaderStatusOverride (enum)OSDriven (default), ForceEnabled, ForceDisabled
IAccessibilityNotificationDispatcherInterface for pushing notifications to the screen reader
IAccessibilityNotificationDispatcher.SendAnnouncement(string)Ambient one-off message; no effect from an iOS button callback
IAccessibilityNotificationDispatcher.SendLayoutChangedNotifies of a local layout change, optionally refocuses a node
IAccessibilityNotificationDispatcher.SendScreenChangedNotifies of a large-scale screen change, optionally refocuses a node
IAccessibilityNotificationDispatcher.SendPageScrolledAnnouncementNotifies that a scroll action completed
AccessibilitySettingsRead-only OS accessibility preferences (Android/iOS only)
AccessibilitySettings.fontScale / .fontScaleChangedOS text-size multiplier and its change event
AccessibilitySettings.isBoldTextEnabled / .boldTextStatusChangedOS bold-text preference and its change event
AccessibilitySettings.isClosedCaptioningEnabled / .closedCaptioningStatusChangedOS captioning preference and its change event
VisionUtility.GetColorBlindSafePaletteColorblind-safe palette helper for general visual accessibility
PlayerLoop.PreLateUpdate.AccessibilityUpdatePlayer-loop point where batched OS accessibility events are processed
PlayerLoop.PostLateUpdate.AccessibilityLateUpdatePlayer-loop point where node frames auto-refresh after window move/resize
Window > Accessibility > Hierarchy ViewerEditor tool for inspecting live AccessibilityNode properties in Play Mode

Advanced Notes

Platform differences (VoiceOver vs. TalkBack vs. Narrator). The Accessibility module is a platform-abstraction layer: you write one hierarchy and one set of node properties, and Unity translates them to each OS's native accessibility tree — but the docs are explicit that "these APIs might result in slight behavior differences across platforms," always in the direction of matching each platform's own native conventions rather than being identical everywhere. Concretely:

  • VoiceOver (iOS/iPadOS, macOS) uses swipe-based gesture navigation (swipe right/left to move focus forward/back through the hierarchy, double-tap to activate — mapped to invoked — swipe up/down for incremented/decremented on sliders/steppers) and generally favors a depth-first traversal of the node tree. iOS is also where AccessibilityHierarchy.MoveNode and reassigning AssistiveSupport.activeHierarchy are documented as having their highest native cost, scaling with subtree/hierarchy size — minimize structural churn on the active hierarchy specifically on this platform. SendAnnouncement is documented to silently no-op when called from inside an iOS button's own callback.
  • TalkBack (Android, API 26+/Android 8.0+) uses a comparable but distinct swipe/explore-by-touch gesture set and reads label/value/hint/role/state through Android's AccessibilityNodeInfo equivalent. Android is the only other platform (besides iOS) where AccessibilitySettings (font scale, bold text, closed captioning) actually returns live OS values — macOS and Windows Players do not support AccessibilitySettings at all, only AssistiveSupport/hierarchy features.
  • Narrator (Windows) and VoiceOver (macOS) are supported by AssistiveSupport/AccessibilityHierarchy per the class docs' platform list, but AccessibilitySettings has no effect there — plan font-scale/caption/bold-text handling as mobile-only behavior, with desktop platforms needing their own settings UI if you want equivalent user control.
  • Regardless of platform, gestures vary but all map to the same underlying node events (invoked, dismissed, scrolled, incremented, decremented) — this is the entire point of the abstraction: you never branch your gameplay code on Application.platform to handle accessibility interaction, only to decide whether to read AccessibilitySettings (mobile) at all.

How the Accessibility Hierarchy relates to the visual UI tree. The two trees are deliberately decoupled: an AccessibilityNode "exists and functions independently of its corresponding visual elements," per the AccessibilityNode class docs — hiding, reordering, or restyling a VisualElement/RectTransform does not automatically change the visibility, position, or content of the node that represents it. This is why every one of the worked examples above wires explicit synchronization: GeometryChangedEvent → frame updates, enabledSelf/interactable state → AccessibilityNode.state, and widget click handlers ↔ invoked. Practical consequences:

  • A node with isActive = true but a frame that was never updated to match its widget's current position will report a stale hit-target to the screen reader (relevant on platforms that use frame-based rather than pure tree-order navigation).
  • Destroying/hiding a VisualElement without also calling RemoveNode (or setting isActive = false) leaves a "ghost" node in the hierarchy that a screen reader can still focus on even though nothing is visually there.
  • The accessibility tree can be shallower or differently shaped than the visual tree by design — the architecture doc recommends flattening it for navigability, so a deeply nested UI Toolkit/uGUI layout should often collapse to a flatter accessibility structure (e.g., group a whole card into one Container node with a synthesized label rather than exposing every internal decorative child).
  • Because the accessibility hierarchy is UI-framework-agnostic, the same mechanism covers pure 3D/2D world objects with no UI representation at all (e.g., an interactable object in the game world) — you build a node for it exactly as you would for a UI Toolkit button, there's just no VisualElement on the other end to keep in sync.
  • Only the hierarchy attached to the application's main window is exposed to screen readers; content on secondary displays/windows has no accessibility representation regardless of how its visual tree is structured.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.