Pass the layout boxes your own renderer computed straight to the accessibility checker, so rules that depend on layout, like WCAG 2.2 target size, get a verdict without Chrome or DOMSnapshot JSON.
Some accessibility rules can’t be decided from HTML and CSS alone. Whether a button is at least 24 by 24 CSS pixels, whether text sits on the background the cascade says it does, or whether a region actually scrolls is known only after layout. Without layout, the checker reports these as needing render and names the facts it would need. Those facts are RenderedFacts. You can parse them from a Chrome DOMSnapshot capture, or, from 1.2.0, build them in memory with RenderedFactsBuilder from whatever layout data you already have.
Here, a toolbar’s two icon buttons are styled to 20 by 20 pixels and sit side by side:
using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Accessibility.Rendered; using RecursiveAI.OneHtml.Accessibility.Validation; using RecursiveAI.OneHtml.Css; Document doc = Document.Parse(""" <!doctype html> <html lang="en"><title>Editor</title> <style>.icon { width: 20px; height: 20px; padding: 0 }</style> <body> <div role="toolbar" aria-label="Formatting"> <button aria-label="Bold" class="icon">B</button><button aria-label="Italic" class="icon">I</button> <button aria-label="Insert link">Link</button> </div> </body></html> """); var options = new CheckOptions { RuleIds = ["target-size"] }; // Without layout, the checker can't know how big a button is. CheckReport before = new Checker().Check(doc, options); Console.WriteLine($"Without facts: needs render {before.Count(Outcome.NeedsRender)}, failed {before.Count(Outcome.Failed)}"); // The boxes your own renderer computed, in CSS pixels: no browser, no DOMSnapshot JSON. var layout = new Dictionary<string, LayoutRect> { ["html"] = new(0, 0, 800, 600), ["body"] = new(8, 8, 784, 32), ["[role=toolbar]"] = new(8, 8, 784, 32), ["[aria-label=Bold]"] = new(8, 14, 20, 20), ["[aria-label=Italic]"] = new(28, 14, 20, 20), ["[aria-label='Insert link']"] = new(52, 8, 56, 32), }; var builder = new RenderedFactsBuilder(doc); builder.SetViewport(800, 600); builder.SetContext(StyleContext.Default); int paintOrder = 0; foreach (var (selector, box) in layout) { builder.AddBox(doc.Root.QuerySelector(selector), new BoxInput { BorderBox = box, PaintOrder = paintOrder++ }); } RenderedFacts facts = builder.Build(); CheckReport after = new Checker().Check(doc, new CheckOptions { RuleIds = ["target-size"], Facts = facts }); foreach (RuleResult result in after.Results.Where(r => r.Outcome is Outcome.Failed or Outcome.Passed)) { Console.WriteLine($"{result.Outcome,-6} {result.Locator}: {result.Message}"); } Console.WriteLine($"With facts: needs render {after.Count(Outcome.NeedsRender)}, failed {after.Count(Outcome.Failed)}, passed {after.Count(Outcome.Passed)}");
Without facts: needs render 3, failed 0 Failed [aria-label="Bold"]: The target is 20 by 20 CSS pixels at 8, 14, under 24 by 24, and another target (20 by 20 at 28, 14) is too close. Failed [aria-label="Italic"]: The target is 20 by 20 CSS pixels at 28, 14, under 24 by 24, and another target (20 by 20 at 8, 14) is too close. Passed [aria-label="Insert link"]: The target is 56 by 32 CSS pixels. With facts: needs render 0, failed 2, passed 1
How to read it:
- Without facts, all three buttons need render: the checker won’t guess a size. With the boxes supplied, Bold and Italic fail WCAG 2.5.8, because each is under 24 by 24 and the other is too close. “Insert link” passes at 56 by 32.
AddBoxtakes a node of the sameDocumentand aBoxInput: the border box in document coordinates and CSS pixels, plus whatever else you have, such as paint order, client and scroll rectangles, layout text, text boxes and the blended background color. Anything you don’t set reads as absent, and a rule that needs it reports needs render again.- Give every element that was laid out a box, ancestors included. An element without a box reads as not laid out, the way
display: noneis. - The builder checks its input the way the snapshot parser does: a node from another document, a second box for one node, or a rectangle with an infinite component is an
ArgumentException.RenderedFactsLimitscaps the number of boxes, text boxes and strings. - The result is the same
RenderedFactstype the snapshot path produces, so it also goes toAccessibilityOptions.Factsfor the accessibility tree, andWithBackgroundSamplesadds pixel samples for contrast.
Rendered facts lists everything the builder can set and how it’s validated. For the snapshot path from a real browser, see Accessibility testing with Playwright.