Lay a page out with OneRender and hand the layout to OneHtml’s accessibility checker, so rules that need rendering, such as color contrast and target size, get a verdict without a browser.
dotnet add package RecursiveAI.OneRender.Accessibility --version 1.0.0 dotnet add package RecursiveAI.OneRender.Image --version 1.0.0 dotnet add package RecursiveAI.OneHtml.Accessibility.Validation --version 1.2.0
.Accessibility produces the facts, .Image provides the bundled fonts (FontProviders) the layout measures text with, and OneHtml’s .Validation package is the checker.
using OneRender; using OneRender.Accessibility; using OneRender.Image; using RecursiveAI.OneHtml.Accessibility.Validation; string html = """ <!doctype html> <html lang="en"><title>Editor</title> <style> body { font: 16px Arial, sans-serif } .icon { width: 20px; height: 20px; padding: 0 } .hint { color: #999; background: #fff } .banner { background: #1d4ed8; color: #fff; padding: 8px } </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> <p class="hint">Drafts are saved every minute.</p> <p class="banner">You are offline.</p> </body></html> """; // Lay the page out with OneRender: boxes, paint order, text boxes and the colors behind text. RenderedPage rendered = AccessibilityFacts.Render(html, new HtmlPageOptions(FontProviders.CreateDefault()) { ViewportWidth = 1280, ViewportHeight = 800 }); var options = new CheckOptions { RuleIds = ["target-size", "color-contrast"], Facts = rendered.Facts }; CheckReport report = new Checker().Check(rendered.Document, options); foreach (RuleResult r in report.Results.Where(r => r.Outcome is Outcome.Failed)) Console.WriteLine($"{r.RuleId,-14} {r.Locator}: {r.Message}"); Console.WriteLine($"failed {report.Count(Outcome.Failed)}, passed {report.Count(Outcome.Passed)}, needs render {report.Count(Outcome.NeedsRender)}"); CheckReport without = new Checker().Check(rendered.Document, new CheckOptions { RuleIds = ["target-size", "color-contrast"] }); Console.WriteLine($"without facts: failed {without.Count(Outcome.Failed)}, needs render {without.Count(Outcome.NeedsRender)}");
color-contrast body > p:nth-of-type(1): The text has a contrast ratio of 2.84:1; 4.50:1 is needed. target-size [aria-label="Bold"]: The target is 20 by 20 CSS pixels at 8, 8.5, under 24 by 24, and another target (20 by 20 at 28, 8.5) is too close. target-size [aria-label="Italic"]: The target is 20 by 20 CSS pixels at 28, 8.5, under 24 by 24, and another target (20 by 20 at 8, 8.5) is too close. failed 3, passed 5, needs render 0 without facts: failed 1, needs render 3
Without facts, the checker can judge the gray hint’s contrast from the cascade alone, but it won’t guess the size of a button: three results need render. With OneRender’s facts, nothing is left undecided. The two 20-pixel icon buttons fail WCAG 2.5.8, and the white-on-blue banner passes contrast.
What the facts hold
rendered.Facts is OneHtml’s RenderedFacts, the same shape a Chrome DOMSnapshot capture gives, so a check reads the same either way:
- Every element’s boxes: border box, client, offset and scroll rectangles, and visibility.
- Paint order and stacking contexts.
- Text boxes in layout order, and the color behind each run of text, blended over white, with the text’s opacity.
- Computed styles in the browser’s serialization, and form state.
The color behind text is left out where one color can’t describe it, such as text over an image or under a filter.
Other ways in
AccessibilityFacts.FromInteractive(page)reads anInteractivePageas it is now, so hover and focus styles show in the facts.AccessibilityFacts.FromPageandFromLayoutwork from a page you already laid out and painted.RenderedFactsLimitsandFactsOptions.BackgroundScanLimitbound the work on hostile pages.
What the facts contain, and where they differ from Chrome’s, is in Rendered page facts. The checker and its rules are in onehtml’s checker docs.