---
title: "Query HTML with CSS selectors"
url: https://recursiveai.org/onehtml/cookbook/css-selectors/
description: "Find elements with QuerySelector, QuerySelectorAll, Matches and Closest using Selectors Level 4, including :has(), :is(), :not() with selector lists, and…"
version: "1.0.0"
---

# Query HTML with CSS selectors

Find elements with QuerySelector, QuerySelectorAll, Matches and Closest using Selectors Level 4, including :has(), :is(), :not() with selector lists, and form-state pseudo-classes.

The query members are on `Node`, so you can query the whole document (`doc.Root`) or any element. They follow the DOM standard: `QuerySelector` returns the first match in document order, and `QuerySelectorAll` returns every match. When nothing matches, `QuerySelector` returns the null node. Check `IsNull` rather than comparing to `null`, because `Node` is a struct.

```csharp
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Selectors;

Document doc = Document.Parse(File.ReadAllBytes("events.html"));
Node root = doc.Root;

// First match, or the null node
Node current = root.QuerySelector("nav a[aria-current=page]");
Console.WriteLine($"Current page: {current.TextContent}");

Node none = root.QuerySelector("aside");
Console.WriteLine($"Has an aside: {!none.IsNull}");

// All matches, in document order; Selectors Level 4 is supported
foreach (Node card in root.QuerySelectorAll("article:has(img:not([alt]))"))
{
    Console.WriteLine($"Card without alt text: {card.GetAttribute("data-event-id")}");
}

foreach (Node link in root.QuerySelectorAll(":is(header, footer) a[href]"))
{
    Console.WriteLine($"{link.LocalName} -> {link.GetAttribute("href")}");
}

// Matches and Closest test an element you already have
Node details = root.QuerySelector("a[href$='/story-time/']");
Console.WriteLine($"Inside a card: {details.Matches(".card a")}, card: {details.Closest(".card").GetAttribute("data-event-id")}");

// Form state pseudo-classes are decided from attributes
Console.WriteLine($"Fields showing a placeholder: {root.QuerySelectorAll("input:placeholder-shown").Count}");

// Compile a selector once when you run it many times
SelectorList external = SelectorList.Parse("a[href^='http']:not([href*='harborstreet'])");
foreach (Node link in root.QuerySelectorAll(external))
{
    Console.WriteLine($"External: {link.GetAttribute("href")}");
}

// A bad selector throws SelectorSyntaxException
try
{
    root.QuerySelector("nav >");
}
catch (SelectorSyntaxException e)
{
    Console.WriteLine($"Rejected: {e.Message}");
}
```

```text
Current page: Events
Has an aside: False
Card without alt text: 114
a -> /
a -> /books/
a -> /events/
a -> /visit/
a -> mailto:hello@example.org
Inside a card: True, card: 114
Fields showing a placeholder: 1
External: https://example.org/repair-cafe
Rejected: Not a valid selector list.
```

What the matcher does, and doesn’t, decide:

- Type selectors and attribute names match HTML elements case-insensitively, and the HTML list of case-insensitive attribute values (`type`, `lang` and others) is honored. Add `i` for anything else, as in `[href^='javascript:' i]`.
- `:checked`, `:disabled`, `:required`, `:placeholder-shown`, `:in-range` and the other form pseudo-classes are decided from attributes. `:valid` and `:invalid` are decided where that is possible without script: `required`, `email`, `url`, `pattern`, `min`, `max` and `step`.
- No script runs, so `:defined` matches every element that isn’t a custom element. `:visited`, `:autofill`, `:popover-open` and `:modal` never match.
- `:hover`, `:focus`, `:focus-visible`, `:active` and `:target` match only when you give an interaction state in `SelectorMatchOptions`. Otherwise they match nothing. The cascade takes the same state through `StyleContext` (see [Compute CSS styles](https://recursiveai.org/onehtml/cookbook/computed-style-csharp/)).
- Pseudo-elements parse but never match an element.
- An unclosed `[` or `(` at the end of the input is closed for you, as CSS Syntax says, so `a[href` is valid. A selector that is really invalid throws `SelectorSyntaxException`.

`SelectorList.Parse` compiles a selector once. Matching against a compiled list allocates nothing, and `QuerySelectorAll` allocates only the result list.

There is no XPath API. The full rules, limits and the web-platform-tests results are in [Selectors](https://recursiveai.org/onehtml/docs/selectors/).

Sixteen selectors measured against AngleSharp, Fizzler and Chrome: [CSS selectors in C#](https://recursiveai.org/onehtml/articles/css-selectors-csharp/).
