---
title: "Getting started with onehtml"
url: https://recursiveai.org/onehtml/docs/getting-started/
description: "Install onehtml for .NET 10, parse an HTML page the way a browser does, query it with CSS selectors, make an edit that leaves every other byte alone…"
version: "1.0.0"
---

# Getting started with onehtml

Install onehtml for .NET 10, parse an HTML page the way a browser does, query it with CSS selectors, make an edit that leaves every other byte alone, compute the accessibility tree, and check the page against WCAG 2.2 AA with SARIF output.

## Install

onehtml targets `net10.0`, so you need the .NET 10 SDK. The three packages share one version; this guide uses 1.0.0.

```shell
dotnet new console -o OneHtmlStart
cd OneHtmlStart
dotnet add package RecursiveAI.OneHtml --version 1.0.0
dotnet add package RecursiveAI.OneHtml.Accessibility --version 1.0.0
dotnet add package RecursiveAI.OneHtml.Accessibility.Validation --version 1.0.0
```

Install only what you use: `RecursiveAI.OneHtml.Accessibility` brings in `RecursiveAI.OneHtml`, and `RecursiveAI.OneHtml.Accessibility.Validation` brings in both. The libraries use nothing outside the .NET base class library, contain no `unsafe` code, and work with NativeAOT and trimming.

The libraries never read files or the network. You load the bytes and hand them over, as the examples below do. Scripts are kept as inert text, and nothing is laid out or painted.

## The sample page

Every step below runs against `events.html`, a 2,051-byte events page for a made-up library. It has a header with a logo and navigation, two event cards, a newsletter form and a footer. It also has two problems an accessibility check should catch: an image with no `alt` and an icon-only submit button.

```html
<article class="card" data-event-id="114">
  <h2>Story time for ages 3&ndash;5</h2>
  <p>Saturdays at 10:30 in the children&rsquo;s room. <a href="/events/story-time/">Details</a></p>
  <img src="/img/story-time.jpg" width=640 height=360>
</article>
```

The snippets are one program, in order. Put these `using` directives at the top of `Program.cs`:

```csharp
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Editing;
using RecursiveAI.OneHtml.Css;
using RecursiveAI.OneHtml.Accessibility;
using RecursiveAI.OneHtml.Accessibility.Validation;
using RecursiveAI.OneHtml.Accessibility.Validation.Catalogue;
```

## Parse a document

`Document.Parse` takes bytes, a string or a `SourceText`. Give it bytes when you have them: it then picks the encoding the way a browser does (byte order mark, then the `Content-Type` you pass in `ParseOptions`, then the `<meta>` prescan) and records how sure it is.

```csharp
byte[] bytes = File.ReadAllBytes("events.html");
Document doc = Document.Parse(bytes);

Console.WriteLine($"Encoding: {doc.Source.Encoding.Name} ({doc.Source.Confidence})");
Console.WriteLine($"Mode: {doc.Mode}, nodes: {doc.NodeCount}");
```

```text
Encoding: UTF-8 (Certain)
Mode: NoQuirks, nodes: 101
```

Parsing follows the WHATWG HTML standard: the tokenizer, every insertion mode, foster parenting, templates and foreign content. Malformed markup doesn’t throw. It becomes the same tree a browser would build. A `Document` is an immutable snapshot that any number of threads can read at once, and `Node` is a small struct handle into it.

## Query with CSS selectors

`QuerySelector` returns the first match, or a null node (check `IsNull`). `QuerySelectorAll` returns every match in document order. Both take Selectors Level 4, including `:has()`, `:is()`, `:where()` and `:not()` with selector lists.

```csharp
Node title = doc.Root.QuerySelector("title");
Console.WriteLine(title.TextContent);

foreach (Node heading in doc.Root.QuerySelectorAll("article.card > h2"))
{
    Node article = heading.Closest("article");
    Console.WriteLine($"{article.GetAttribute("data-event-id")}: {heading.TextContent}");
}

Node missing = doc.Root.QuerySelector("main img:not([alt])");
Console.WriteLine($"No alt: {missing.GetAttribute("src")}, has width: {missing.HasAttribute("width")}");
```

```text
Events | Harbor Street Library
114: Story time for ages 3–5
115: Repair café
No alt: /img/story-time.jpg, has width: True
```

Character references are already decoded: `&ndash;` reads as `–` and `caf&eacute;` as `café`. `GetAttribute` returns `null` when the attribute is absent and `""` when it is present but empty.

## Make a lossless edit and write it back

`doc.Apply(edits)` takes a list of edits aimed at nodes of the snapshot. Either all of them apply or none do. The output is spliced from the original bytes: only the bytes an edit touches change. The engine re-parses the result and checks that it matches the expected tree before returning it. The old snapshot and its handles stay valid.

```csharp
EditResult result = doc.Apply(new[]
{
    Edit.SetAttribute(missing, "alt", "Children listening to a story"),
});

if (!result.Succeeded)
{
    throw new InvalidOperationException(result.Failure);
}

byte[] output = result.Document!.ToLosslessBytes();
File.WriteAllBytes("events.edited.html", output);

EditedRange range = result.Changes!.Ranges.Single();
Console.WriteLine($"Inserted at byte {range.OldByteRange.Start}: [{range.NewText}]");

int at = range.OldByteRange.Start;
int grew = output.Length - bytes.Length;
bool headSame = bytes.AsSpan(0, at).SequenceEqual(output.AsSpan(0, at));
bool tailSame = bytes.AsSpan(range.OldByteRange.End).SequenceEqual(output.AsSpan(range.OldByteRange.End + grew));
Console.WriteLine($"{bytes.Length} -> {output.Length} bytes; before the edit identical: {headSame}, after: {tailSame}");
```

```text
Inserted at byte 1086: [ alt="Children listening to a story"]
2051 -> 2087 bytes; before the edit identical: True, after: True
```

The unquoted `width=640 height=360`, the `&ndash;` reference, the comments and the indentation all stay as they were. `diff` shows one changed line:

```text
33c33
<       <img src="/img/story-time.jpg" width=640 height=360>
---
>       <img src="/img/story-time.jpg" width=640 height=360 alt="Children listening to a story">
```

The other operations are `RemoveAttribute`, `ReplaceText`, `InsertBefore`, `InsertAfter`, `Prepend`, `Append`, `Wrap`, `Unwrap`, `Replace`, `Remove`, `SetStyle` and `RemoveStyle`. [Edit HTML without reformatting it](https://recursiveai.org/onehtml/cookbook/edit-html-csharp-lossless/) shows several at once.

## Compute the accessibility tree

The accessibility tree is what a browser gives to screen readers: a role, an accessible name and states for each node that matters. `AccessibilityTree.Build` computes it from the document and its cascade (WAI-ARIA 1.2, HTML-AAM and AccName 1.2), using Chromium’s choices where the specifications leave room. No browser is involved.

```csharp
DocumentStyles styles = DocumentStyles.Create(doc);
AccessibilityTree tree = AccessibilityTree.Build(doc, styles);

string[] interesting = { "heading", "link", "button", "image", "textbox" };
foreach (AxNode node in tree.Nodes.Where(n => !n.IsIgnored && interesting.Contains(n.Role)))
{
    Console.WriteLine($"{node.Role,-8} \"{node.Name.Value}\" from {node.Name.Source}");
}
```

```text
link     "Harbor Street Library" from Contents
image    "Harbor Street Library" from NativeAttribute
link     "Books" from Contents
link     "Events" from Contents
link     "Visit" from Contents
heading  "Upcoming events" from Contents
heading  "Story time for ages 3–5" from Contents
link     "Details" from Contents
image    "" from None
heading  "Repair café" from Contents
link     "Partner site" from Contents
image    "Volunteers at a workbench fixing a lamp" from NativeAttribute
textbox  "Your email" from Placeholder
button   "" from None
link     "hello@example.org" from Contents
button   "Back to top" from Contents
```

This tree was built from the original `doc`, so the story-time image still has no name. The submit button has none either: its only content is an `aria-hidden` icon. The logo link takes its name from the `alt` of the image inside it. The email field is named only by its placeholder.

## Check the page against WCAG 2.2 AA

`Checker` runs one rule catalogue that covers axe-core’s WCAG and best-practice rules and every W3C-approved ACT rule. Each rule is labeled with the WCAG criteria it checks. A profile picks the rules for a standard: `Wcag20AA`, `Wcag21AA`, `Wcag22AA`, or `All`, which adds best practices.

```csharp
var options = new CheckOptions { Profile = ValidationProfile.Wcag22AA };
CheckReport report = new Checker().Check(doc, options);

foreach (RuleResult failure in report.WithOutcome(Outcome.Failed))
{
    Console.WriteLine($"{failure.RuleId} at {failure.Locator}: {failure.Message}");
}
Console.WriteLine($"Failed {report.Count(Outcome.Failed)}, needs review {report.Count(Outcome.NeedsReview)}, " +
                  $"needs render {report.Count(Outcome.NeedsRender)}, passed {report.Count(Outcome.Passed)}");

string sarif = report.ToSarifJson(new ExportOptions { Url = "events.html", Indented = true });
File.WriteAllText("events.sarif", sarif);
```

```text
button-name at form > button: The button has no accessible name.
image-alt at #content > article:nth-of-type(1) > img: The image has no accessible name.
Failed 2, needs review 18, needs render 10, passed 62
```

Every result has one of five outcomes:

- `Failed`, `Passed` and `Inapplicable` are decided.
- `NeedsReview` means markup and CSS can’t settle it, for example whether a heading describes its section. A person has to look.
- `NeedsRender` means the rule needs layout captured from a browser. Here all 10 are `target-size`, which needs the border boxes of each target and its neighbors. You can supply those as `RenderedFacts` (see [Rendered facts](https://recursiveai.org/onehtml/docs/rendered-facts/)), and `FactsMissing` on each result lists what is needed.

The checker doesn’t replace human review. It reports what it can’t decide instead of guessing.

`events.sarif` is SARIF 2.1.0. Each failure has its rule, its line and column, its byte offset and a source snippet. The rule’s entry in the file lists the WCAG criteria it checks.


```json
{
  "ruleId": "button-name",
  "ruleIndex": 28,
  "kind": "fail",
  "level": "error",
  "rank": 90,
  "message": {
    "text": "The button has no accessible name."
  },
  "locations": [
    {
      "physicalLocation": {
        "artifactLocation": {
          "uri": "events.html",
          "index": 0
        },
        "region": {
          "startLine": 45,
          "startColumn": 7,
          "endLine": 45,
          "endColumn": 29,
          "byteOffset": 1584,
          "byteLength": 22,
          "snippet": {
            "text": "<button type=\"submit\">"
          }
        }
      }
    }
  ]
}
```

That is an excerpt of one result. The full result also has a logical location, a fingerprint and properties such as `outcome` and `locator`. Review results have `kind: "review"` and `level: "none"`, so filter on `kind` as well as `level`. [SARIF export](https://recursiveai.org/onehtml/docs/sarif/) gives the full mapping.

## The packages

| Package | What it gives you |
|---|---|
| `RecursiveAI.OneHtml` | The WHATWG HTML parser with encoding sniffing, the DOM as immutable snapshots with a byte-level source map, Selectors Level 4, unique locators, a CSS parser, the cascade under a `StyleContext` (viewport, media features, focus, hover), and lossless editing of HTML and CSS with a JSON edit format you can save and replay. |
| `RecursiveAI.OneHtml.Accessibility` | The accessibility tree (WAI-ARIA 1.2, HTML-AAM, AccName 1.2, Core-AAM, SVG-AAM) with each role, name and hidden state traced to its source, incremental updates after edits, a reading view, navigation lists, tree diffs, export in Chrome’s AX-tree shape, and `RenderedFacts` from a Chrome `DOMSnapshot` you supply. |
| `RecursiveAI.OneHtml.Accessibility.Validation` | The rule catalogue labeled by WCAG version, level and criterion; WCAG 2.0, 2.1 and 2.2 AA profiles; incremental re-checks after an edit; suggested edits where exactly one fix is correct; and native JSON, axe-compatible JSON and SARIF 2.1.0 output. |

All three are licensed MIT OR Apache-2.0, at your option.

## Where next

Articles: [Parse HTML in C#](https://recursiveai.org/onehtml/articles/parse-html-csharp/), [Migrate from HtmlAgilityPack](https://recursiveai.org/onehtml/articles/migrate-from-htmlagilitypack/), [HtmlAgilityPack vs AngleSharp](https://recursiveai.org/onehtml/articles/htmlagilitypack-vs-anglesharp/).

- Recipes: [parse HTML from a string, file or stream](https://recursiveai.org/onehtml/cookbook/parse-html/), [CSS selectors](https://recursiveai.org/onehtml/cookbook/css-selectors/), [extract visible text](https://recursiveai.org/onehtml/cookbook/extract-visible-text/), [compute styles](https://recursiveai.org/onehtml/cookbook/computed-style-csharp/), [WCAG checks in CI with SARIF](https://recursiveai.org/onehtml/cookbook/wcag-checks-ci-sarif/) and [apply suggested fixes](https://recursiveai.org/onehtml/cookbook/apply-suggested-fixes/).
- How editing works, including fallback when a splice would change how the page parses: [Editing](https://recursiveai.org/onehtml/docs/editing/).
- The selector support and its known gaps: [Selectors](https://recursiveai.org/onehtml/docs/selectors/).
- The cascade and computed values: [Cascade](https://recursiveai.org/onehtml/docs/cascade/).
- How the tree is built and where Chromium’s choices are used: [Accessibility tree](https://recursiveai.org/onehtml/docs/accessibility-tree/) and [Chromium profile](https://recursiveai.org/onehtml/docs/accessibility-chromium-profile/).
- Rule selection, private rules and re-checking: [The checker](https://recursiveai.org/onehtml/docs/checker/). Coverage per WCAG criterion: [Coverage](https://recursiveai.org/onehtml/docs/coverage/).

Guides: [Web scraping in C#](https://recursiveai.org/onehtml/articles/web-scraping-csharp/) and [CSS selectors in C#](https://recursiveai.org/onehtml/articles/css-selectors-csharp/).
