← Back to docs
JustHTML supports a comprehensive subset of CSS selectors for querying the DOM.
- Selector parsing is cached internally (LRU, up to 512 distinct selector strings). Repeating the same selector across many calls avoids re-parsing overhead.
- Simple tag-only selectors like
"div" use a fast path (descendant scan without running the full selector matcher).
- If you generate lots of unique selectors dynamically, expect some parse overhead once the cache churns.
- Selector evaluation uses resource limits for selector length, selector shape, parse depth, match operations, and string materialization work. These limits are intentionally conservative for untrusted input.
Default selector limits:
| Limit | Default | Applies to |
|---|
max_length | 8192 | Selector string length |
max_list_items | 256 | Distinct comma-separated selector entries |
max_compound_simple_selectors | 512 | Distinct simple selectors in one compound selector |
max_complex_selector_parts | 512 | Combinator-linked parts in one complex selector |
max_parse_depth | 100 | Nested functional pseudo-class parsing, such as nested :not(...) |
max_match_depth | 100 | Nested selector matching |
max_match_steps | 100_000_000 | Selector matching operations |
max_match_bytes | 100_000_000 | Attribute/text string work during matching |
Direct query(...) and matches(...) calls use the defaults. Sanitization transform pipelines can raise limits for trusted workloads with SanitizationPolicy(selector_limits=...); see Transforms.
| Selector | Example | Description |
|---|
| Tag | div | Elements by tag name |
| Class | .intro | Elements with class |
| ID | #main | Element with ID |
| Universal | * | All elements |
doc.query("div") # All <div> elements
doc.query_one("div") # First matching <div>, or None
doc.query(".intro") # All elements with class="intro"
doc.query("#main") # Element with id="main"
doc.query("*") # All elements
| Selector | Example | Description |
|---|
| Has attribute | [href] | Elements with attribute |
| Exact match | [type="text"] | Exact attribute value |
| Starts with | [href^="https"] | Attribute starts with value |
| Ends with | [href$=".pdf"] | Attribute ends with value |
| Contains | [href*="example"] | Attribute contains value |
| Word match | [class~="active"] | Space-separated word match |
| Prefix match | [lang|="en"] | Value or value followed by - |
doc.query("[href]") # All elements with href attribute
doc.query('[type="submit"]') # Exact match
doc.query('[href^="https://"]') # Links starting with https://
doc.query('[src$=".png"]') # Images ending with .png
doc.query('[class*="btn"]') # Classes containing "btn"
| Combinator | Example | Description |
|---|
| Descendant | div p | <p> anywhere inside <div> |
| Child | div > p | <p> direct child of <div> |
| Adjacent | h1 + p | <p> immediately after <h1> |
| Sibling | h1 ~ p | Any <p> sibling after <h1> |
doc.query("div p") # Paragraphs anywhere in a div
doc.query("ul > li") # Direct list items
doc.query("h1 + p") # First paragraph after h1
doc.query("h1 ~ p") # All paragraphs after h1
| Selector | Description |
|---|
:first-child | First child of parent |
:last-child | Last child of parent |
:only-child | Only child of parent |
:nth-child(n) | Nth child (1-indexed) |
:nth-last-child(n) | Nth child from end |
:first-of-type | First of its type in parent |
:last-of-type | Last of its type in parent |
:only-of-type | Only of its type in parent |
:nth-of-type(n) | Nth of its type |
:nth-last-of-type(n) | Nth of its type from end |
doc.query("li:first-child") # First list item
doc.query("li:last-child") # Last list item
doc.query("li:nth-child(2)") # Second list item
doc.query("li:nth-child(odd)") # Odd list items (1st, 3rd, 5th...)
doc.query("li:nth-child(even)") # Even list items (2nd, 4th, 6th...)
doc.query("li:nth-child(3n)") # Every 3rd item
doc.query("li:nth-child(3n+1)") # 1st, 4th, 7th... (formula: An+B)
| Selector | Description |
|---|
:empty | Elements with no children |
:root | Document root element |
:contains("text") | Non-standard (jQuery-style): elements whose descendant text contains text |
:comment | Non-standard: comment nodes (#comment) |
doc.query("p:empty") # Empty paragraphs
doc.query(":root") # The <html> element
doc.query(":comment") # Comment nodes
| Selector | Description |
|---|
:not(selector) | Elements not matching selector |
doc.query("div:not(.hidden)") # Visible divs
doc.query("input:not([disabled])") # Enabled inputs
Combine multiple selectors with commas:
doc.query("h1, h2, h3") # All headings
doc.query(".btn, [type='submit']") # Buttons and submit inputs
Chain selectors without spaces:
doc.query("div.container") # <div class="container">
doc.query("input[type='text']") # <input type="text">
doc.query("li.active:first-child") # First li with class active
# Navigation links
doc.query("nav a")
# Active menu item
doc.query("nav li.active > a")
# Form inputs except hidden
doc.query("form input:not([type='hidden'])")
# Alternating table rows
doc.query("tr:nth-child(even)")
# First paragraph in each container
doc.query("div > p:first-of-type")
# External links
doc.query('a[href^="http"]:not([href*="mysite.com"])')