Similar to a video game, Figma’s canvas takes over rendering from the browser to maximize performance. This lets us deliver powerful features like infinite zoom and real-time multiplayer. But because we don’t use traditional HTML and DOM to render Figma’s canvas, we don’t get any of the accessibility features that are built into the browser by default.To make Figma accessible to more people, we created a Mirror DOM structure that could stay in sync with any Figma design. Here, we’ll go behind the scenes of how we made it possible for anyone to navigate a Figma file with a screen reader, hear changes as they’re announced, and operate the editor with a keyboard.Learn more about Figma’s keyboard and screen reader accessibility improvements.Synthesizing the DOMBrowser engines have a concept called the accessibility tree: a data structure distilled from the document containing the non-visual information needed for assistive technologies to work. When a screen reader user executes the “go to next form field” command, the accessibility tree tells the screen reader where to go. The browser builds this tree from DOM, semantic HTML, ARIA attributes, and some additional computed state.In a typical web app, every component has its own DOM element like <button>, <p>, or <img>. But because Figma doesn’t use HTML for rendering, the canvas has only one <input> element that holds focus no matter how many layers are in the design. This meant that the browser’s accessibility tree was virtually empty for a Figma file.To solve this, we added back synthetic DOM elements for the canvas so that screen readers could navigate and edit files.How the system worksThe scenegraph is the data structure of nodes that is rendered by Figma’s canvas.Behind the canvas, invisible to sighted users, we render DOM elements mirroring the parts of the scenegraph that matter for assistive technology. There are four collaborating systems:A Figma-internal “accessibility tree” that caches the accessibility details of every design layer, and makes surgical updates rather than rebuilding from scratch as edits are made.A “Mirror DOM” React component that is responsible for actually putting elements into the DOM, using the internal accessibility tree as reference.A bidirectional synchronization system for selection. When you select a node in the canvas, the corresponding DOM element receives focus. Conversely, we update the canvas selection when screen reader tools are used to navigate the Mirror DOM.An announcement system that alerts the user to edits and other non-navigational changes—nudging, tool switching, and anything else that might be obvious to a sighted user.The internal accessibility treeTo determine which DOM elements to render and how, we worked backward from what the browser should know in order to build its accessibility tree. So we built our own internal accessibility tree, which captures the non-visual information that screen readers would require for any given Figma document.For each layer in the document, we create an “accessible summary” that will be read out by screen readers. This summary can depend on the context and what kind of application the user is in. For example, in prototypes, we can omit most of the editing features and only emulate the content to the end viewer: just the text of text fields, or a button role for an item with a click interaction. On the other hand, autolayout frames, which are excluded from the accessibility tree for prototypes, need to be included when the user is editing a document.After summarizing layers one by one, we walk the tree top-to-bottom, flattening out any omitted nodes. While we fully construct our internal accessibility tree when first loading a document, we monitor edits as the session goes on so that we can surgically update our tree, avoiding costly rebuilds.The Mirror DOMWith our accessibility tree ready, we can generate the DOM. This is handled by a React component that renders itself recursively, with each instance of that component subscribing to changes in the accessibility tree for one specific design layer.JSXfunction ScreenReaderElement({ layerId }: { layerId: string }) {