Recursive AI

Check rendered facts without a browser snapshot

All onehtml recipes

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:

C#
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)}");
Text
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.
  • AddBox takes a node of the same Document and a BoxInput: 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: none is.
  • 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. RenderedFactsLimits caps the number of boxes, text boxes and strings.
  • The result is the same RenderedFacts type the snapshot path produces, so it also goes to AccessibilityOptions.Facts for the accessibility tree, and WithBackgroundSamples adds 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.