CSS selectors

knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

zuhtml implements a deliberately small subset of CSS Selectors and rejects everything else with an error. A selector either works exactly as it would in a browser or fails loudly: it never matches something partly.

library(zuhtml)
doc <- html_parse('
<div id=main class="card wide">
  <h2 class=title>One</h2>
  <p class="a b" lang=en-US>first</p>
  <p title="">second</p>
  <ul><li>1<li>2<li>3<li>4</ul>
</div>')

What is supported

| Kind | Examples | |---|---| | Type and universal | p, * | | ID and class | #main, .card.wide | | Attributes | [title], [lang=en-US], [class~=a], [lang\|=en], [href^=http], [href$=".pdf"], [href*=shop], with an i or s flag | | Combinators | div p, div > p, h2 + p, h2 ~ p, and lists: h2, p | | Pseudo-classes | :scope, :root, :empty, :first-child, :last-child, :only-child, :nth-child(an+b), :nth-of-type(an+b), :not() of one compound selector |

html_text(html_elements(doc, "h2 ~ p"))
html_text(html_elements(doc, "li:nth-child(odd)"))
html_text(html_elements(doc, "p:not([title])"))
html_text(html_elements(doc, "[lang|=en]"))

What is rejected

:has(), other pseudo-classes, pseudo-elements (including Scrapy's ::text and ::attr()), namespace prefixes and the of S form of :nth-child() are errors. The error says where:

try(html_elements(doc, "div:has(> h2)"))
try(html_elements(doc, "p::text"))

Handle them by class, zuhtml_selector_error; the condition carries the selector, the position and whether the form is unsupported or malformed.

Case

As in a browser on an HTML page: element and attribute names match HTML elements whatever their case, and SVG or MathML elements exactly. IDs and classes are case-sensitive. Attribute values are case-sensitive, except for the attributes HTML lists as case-insensitive (type, lang and others); an i or s flag overrides either.

length(html_elements(doc, "P"))
length(html_elements(doc, ".CARD"))
length(html_elements(doc, "[lang=EN-US]"))
length(html_elements(doc, "[lang=EN-US s]"))

Where the search looks

Searching a document includes its <html> element. Searching below an element excludes the element itself, unless :scope names it:

main <- html_element(doc, "#main")
length(html_elements(main, ".card"))
length(html_elements(main, ":scope.card"))
html_name(html_elements(main, ":scope > *"))

The rest of a selector may match outside the context, as querySelectorAll() does: "body h2" below #main finds the <h2> even though <body> is outside it.

html_elements() on several nodes returns the union of their matches, without duplicates, in document order. html_element() returns one result per input node, a missing node where there is no match: use it to pull one field out of each of many records.

<template> contents are never searched; reach them with html_template_content().



Try the zuhtml package in your browser

Any scripts or data that you put into this service are public.

zuhtml documentation built on Oct. 6, 2026, 5:06 p.m.