Skip to main content

CSS Selector Reference

Note: The selectors documented below work in stylesheet cascade matching. The query_selector and query_selector_all APIs in wgpu-html-tree additionally support child (>), sibling (+ / ~) combinators, attribute selectors ([attr], [attr=val], etc.), and many pseudo-classes including :first-child, :last-child, :nth-child(), :not(), :is(), :where(), :has(), :root, :scope, :lang(), :dir(), :focus-within, :checked, :disabled, :enabled, :required, :optional, :read-only, :read-write, :placeholder-shown, :first-of-type, :last-of-type, :nth-of-type(), and :nth-last-child().

Selector Grammar

wgpu-html's stylesheet parser supports a flat CSS selector syntax without at-rules or complex pseudo-classes. Each selector is a compound of optional tag, optional #id, optional .class(es), and optional static pseudo-classes, separated by a descendant combinator (whitespace) or comma.

selector_list = selector ("," selector)*
selector = compound (whitespace compound)*
compound = tag? id? class* pseudo_class* | "*" class* pseudo_class*

Tag Selectors

Match elements by their HTML tag name:

div { display: block; }
p { margin: 1em 0; }
h1 { font-size: 2em; }
input { padding: 4px 8px; }

ID Selectors

Match a single element by its id attribute. Prefixed with #:

#main { width: 800px; }
#sidebar { background-color: #f0f0f0; }

IDs use the by_id index for O(1) lookup during cascade.

Class Selectors

Match elements by their class attribute. Prefixed with .:

.highlight { background-color: yellow; }
.button { padding: 8px 16px; }

Multi-class selectors require all listed classes to match:

.button.primary { background-color: blue; }
/* Matches: <div class="button primary"> */
/* Does NOT match: <div class="button"> */

Universal Selector

The * selector matches any element:

* { box-sizing: border-box; }

When combined with classes, *.button is equivalent to .button.

Descendant Combinator

Whitespace between compounds represents the descendant combinator — it matches an element that is a descendant (at any depth) of the first compound:

nav a { color: blue; }
/* Matches <a> inside <nav>, at any nesting level */

.card .title { font-weight: bold; }
/* Matches .title that is a descendant of .card */

div:hover .tooltip { display: block; }
/* Matches .tooltip inside a hovered div */

The cascade engine builds an ancestor chain for each element during the recursive walk. div p matches an element <p> if any ancestor in the chain is <div>.

Comma-Separated Selector Lists

Commas group multiple selectors sharing the same declarations:

h1, h2, h3 {
font-family: sans-serif;
color: #333;
}

#sidebar .link, #footer .link {
color: #666;
}

The comma separates the full selector — the descendant combinator does not distribute over commas.

Pseudo-Classes in Cascade Matching

The stylesheet parser and cascade support three dynamic pseudo-classes in selector matching:

  • :hover — matches when the element's path is a prefix of the document's hover path
  • :active — matches when the element's path is a prefix of the document's active path
  • :focus — matches when the element's path exactly equals the document's focus path
a:hover {
color: red;
text-decoration: underline;
}

button:active {
background-color: #0044cc;
}

input:focus {
border-color: blue;
}

Pseudo-classes can appear on ancestor compounds in descendant selectors:

.row:hover .actions {
visibility: visible;
}

Other pseudo-classes like :first-child and :nth-child() are supported in the query_selector API but not in stylesheet cascade matching.

Specificity Calculation

Selector specificity is computed as a packed integer:

specificity = (id_count << 16) | (class_count << 8) | tag_count
SelectorIDClassTagSpecificity
p0011
.button010256
div.button011257
#main10065536
#main .button11065792
*0000
nav a0022

Higher specificity wins. When specificity is equal, source order determines the winner (later rules override earlier rules).

The cascade sorts matched rules by (specificity, sheet_index, rule_index). The UA stylesheet uses tag-only selectors, so any author rule with equal or higher specificity wins.

Code Examples

Simple Selector Example

/* Tag selector — specificity 1 */
p { color: #333; }

/* Class selector — specificity 256 */
.note { color: blue; }

/* ID selector — specificity 65536 */
#banner { color: red; }

/* Descendant — specificity 2 */
article p { line-height: 1.6; }

/* Combined — specificity 257 */
p.warning { color: orange; font-weight: bold; }

Specificity Override Example

/* specificity: 0,0,1 */
p { color: black; }

/* specificity: 0,1,0 — wins over p */
.error { color: red; }

/* specificity: 0,1,1 — wins over both */
p.error { color: darkred; }

/* specificity: 1,0,1 — wins over everything above */
#container p { color: navy; }

Hover with Descendant

.card {
border: 1px solid #ddd;
padding: 16px;
}

.card:hover {
border-color: blue;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.card:hover .title {
color: blue;
}

.card:hover .actions {
display: flex;
}

Rust: Selector Matcher API

use wgpu_html_style::{matches_selector, matches_selector_in_tree, MatchContext};

// Match without ancestor context
let selector = /* parsed Selector */;
let matches = matches_selector(&selector, &element);

// Match with ancestor chain (for descendant combinators)
let ancestors: Vec<&Element> = vec![&parent_element];
let matches = matches_selector_in_tree(&selector, &element, &ancestors);

// Match with dynamic pseudo-class state
let ctx = MatchContext {
is_hover: true,
is_active: false,
is_focus: false,
is_root: false,
is_first_child: true,
is_last_child: false,
};
let matches = matches_selector_with_context(&selector, &element, &ctx);