Recursive AI

Extract visible text from HTML

All onehtml recipes

Get the readable text of a page without its script and style contents, either with a short walk over the text nodes or with the cascade-aware reading view, which also drops content hidden by CSS, the hidden attribute or aria-hidden.

TextContent follows the DOM standard, which includes the text of script and style elements. That is correct, but it is rarely what you want for search indexing, previews or diffs. There are two ways to get the readable text.

C#
using System.Text;
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Css;
using RecursiveAI.OneHtml.Accessibility;
 
Document doc = Document.Parse(File.ReadAllBytes("events.html"));
Node body = doc.Root.QuerySelector("body");
 
// TextContent follows the DOM standard: script and style text is included
Console.WriteLine($"TextContent has the script: {body.TextContent!.Contains("addEventListener")}");
 
// Walk the text nodes yourself and skip the elements that never render text
var text = new StringBuilder();
foreach (Node node in body.Descendants)
{
    if (node.Kind == NodeKind.Text && node.ParentElement.Closest("script, style, template, noscript").IsNull)
    {
        text.Append(node.Data);
    }
}
string plain = string.Join(' ', text.ToString().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
Console.WriteLine(plain);
 
// Or let the cascade decide: the reading view drops display:none, hidden and aria-hidden content
Document page = Document.Parse("""
    <style>.promo { display: none }</style>
    <h1>Opening hours</h1>
    <p class="promo">Members save 10%</p>
    <p hidden>Draft: holiday hours</p>
    <p>Monday to Friday, 9 to 6. <span aria-hidden="true">★</span></p>
    <script>console.log("not text")</script>
    """);
var tree = AccessibilityTree.Build(page, DocumentStyles.Create(page));
Console.WriteLine(tree.CreateReadingView().ToText());
Text
TextContent has the script: True
Books Events Visit Upcoming events Story time for ages 3–5 Saturdays at 10:30 in the children’s room. Details Repair café Bring a lamp, a toaster or a torn jacket. Volunteers fix what they can. Partner site © 2026 Harbor Street Library · [email protected] Back to top
heading level 1: Opening hours
Monday to Friday, 9 to 6.

The text-node walk is fast and needs only the parser. It skips script, style, template and noscript, so the inline script at the end of the events page is gone. It knows nothing about CSS: text inside display: none or hidden would still be there. Use node.ParentElement.Closest(...), because Closest starts from an element and a text node isn’t one.

The reading view reads the page from its accessibility tree, which is built with the cascade. It leaves out text hidden by display: none, visibility: hidden, content-visibility: hidden, the hidden attribute, a closed details, inert and aria-hidden, and it doesn’t read script. Each line is a step: text runs, plus announced elements such as heading level 1: Opening hours. Headings and other announced elements come through as element items rather than ReadingItemKind.Text items, so keep their text too if you want every visible word; Extract visible text from HTML in C# shows a complete version and compares HtmlAgilityPack and AngleSharp.

Limits of the reading view:

  • It decides from markup and CSS alone. Hiding that depends on layout, such as zero size, clipping or off-screen positioning, isn’t known, so that text is still read. Chrome doesn’t hide such content from its accessibility tree either.
  • The wording is generic English. It is an approximation, not what any particular screen reader says. Reading view has the rules.
  • Script doesn’t run. For a page built by script, parse the browser’s serialized DOM instead of the HTML the server sent.