This page covers how CSS selectors are represented internally, parsed from text, matched against DOM elements, and what code-generation pipeline produces the pseudo-class and pseudo-element enumerations that the engine uses. For how matched selectors are combined with cascade and specificity to compute final styles, see the Style Computation page (4.2). For the CSS tokenizer and rule parser that invokes selector parsing, see CSS Parsing (4.3).
All selector data lives in Libraries/LibWeb/CSS/Selector.h. The spec defines a complex selector as a chain of compound selectors joined by combinators. The code maps directly to this hierarchy.
Data structure summary:
| Class / Type | Role |
|---|---|
Selector | One complex selector; holds Vector<CompoundSelector> |
Selector::CompoundSelector | One compound selector; holds a Combinator and Vector<SimpleSelector> |
Selector::SimpleSelector | One atomic unit (tag, class, id, attribute, pseudo-class, pseudo-element, nesting, or invalid) |
Selector::SimpleSelector::PseudoClassSelector | Payload for a pseudo-class: type enum, An+B pattern, argument selector list, language ranges, etc. |
Selector::SimpleSelector::Attribute | Payload for an attribute selector: match type, qualified name, value, case flag |
Selector::SimpleSelector::QualifiedName | Namespace-aware name used for tag names and attribute names |
SelectorList | Vector<NonnullRefPtr<Selector>> — a comma-separated group of complex selectors |
Sources: Libraries/LibWeb/CSS/Selector.h1-272
SimpleSelector::Type EnumUniversal — *
TagName — div, span
Id — #foo
Class — .bar
Attribute — [href^=https]
PseudoClass — :hover, :nth-child()
PseudoElement — ::before, ::slotted()
Nesting — & (CSS nesting)
Invalid — retained for forgiving lists / nesting serialization
Sources: Libraries/LibWeb/CSS/Selector.h76-86
Combinator Enum| Enumerator | CSS symbol | Meaning |
|---|---|---|
None | (none) | First compound selector in a selector |
Descendant | (whitespace) | Anywhere in subtree |
ImmediateChild | > | Direct child |
NextSibling | + | Adjacent sibling |
SubsequentSibling | ~ | Any subsequent sibling |
Column | || | Table column (not yet fully implemented) |
Sources: Libraries/LibWeb/CSS/Selector.h192-199
Diagram: Selector data model — class relationships
Sources: Libraries/LibWeb/CSS/Selector.h75-253
Selector parsing is implemented in Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp as methods on the Parser class (part of the broader CSS parser described in 4.3).
| Method | Returns | Use case |
|---|---|---|
parse_as_selector() | Optional<SelectorList> | Top-level selector (e.g., style rule) |
parse_as_relative_selector() | Optional<SelectorList> | :has() argument |
parse_as_pseudo_element_selector() | Optional<PseudoElementSelector> | Standalone ::pseudo parsing |
Sources: Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp17-61
Diagram: Selector parsing call hierarchy
Sources: Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp87-205
parse_a_selector_list() accepts a SelectorParsingMode parameter:
SelectorParsingMode::Standard: Any invalid selector causes the whole list to fail.SelectorParsingMode::Forgiving: Invalid selectors are wrapped in a SimpleSelector::Invalid and retained. Used for :is(), :where(), and :has() argument lists so that unknown selectors silently match nothing rather than invalidating the rule.Sources: Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp86-116
parse_selector_qualified_name() handles the three namespace forms:
|E → NamespaceType::None (no namespace)ns|E → NamespaceType::Named (declared namespace prefix)*|E → NamespaceType::AnyE → NamespaceType::DefaultUndeclared namespace prefixes are rejected immediately.
Sources: Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp236-313
The PseudoClass and PseudoElement C++ enumerations and associated metadata functions are generated at build time from JSON definition files, not written by hand.
Diagram: Code generation pipeline for pseudo-classes and pseudo-elements
Sources: Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp1-200 Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp1-350
PseudoClasses.json SchemaEach entry in Libraries/LibWeb/CSS/PseudoClasses.json1-200 describes one pseudo-class:
The argument field drives three things:
PseudoClassMetadata::ParameterType is assigned (e.g., ANPlusB, ForgivingSelectorList, RelativeSelectorList, Ident, LanguageRanges, LevelList)Legacy aliases (like -webkit-autofill) map to an existing enum value instead of generating a new one.
PseudoElements.json SchemaEntries in Libraries/LibWeb/CSS/PseudoElements.json1-147 describe pseudo-elements. Key fields:
| Field | Meaning |
|---|---|
alias-for | Maps this name to another PseudoElement enum value |
property-whitelist | CSS properties this pseudo-element accepts (others are rejected) |
type | "function" if it takes an argument (e.g., ::slotted, ::part) |
function-syntax | Grammar for the argument (e.g., <compound-selector>, <ident>+) |
is-allowed-in-has | Whether this pseudo-element can appear inside :has() |
is-pseudo-root | Whether this pseudo-element creates a stacking root (e.g., ::view-transition) |
| Function | Source file |
|---|---|
pseudo_class_from_string(StringView) | PseudoClass.cpp |
pseudo_class_name(PseudoClass) | PseudoClass.cpp |
pseudo_class_metadata(PseudoClass) | PseudoClass.cpp |
pseudo_element_from_string(StringView) | PseudoElement.cpp |
pseudo_element_name(PseudoElement) | PseudoElement.cpp |
pseudo_element_metadata(PseudoElement) | PseudoElement.cpp |
is_has_allowed_pseudo_element(PseudoElement) | PseudoElement.cpp |
pseudo_element_supports_property(PseudoElement, PropertyID) | PseudoElement.cpp |
Sources: Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp40-200 Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp40-350
The matching engine lives in Libraries/LibWeb/CSS/SelectorEngine.cpp, in the Web::SelectorEngine namespace.
The caller provides a MatchContext that accumulates state shared across multiple match calls within one style rule evaluation.
Sources: Libraries/LibWeb/CSS/SelectorEngine.h56-58
MatchContext Fields| Field | Type | Purpose |
|---|---|---|
style_sheet_for_rule | GC::Ptr<CSSStyleSheet const> | Needed to resolve named namespaces in attribute selectors |
subject | GC::Ptr<Element const> | The element the full selector is being matched against (used by :has() metadata) |
slotted_element | GC::Ptr<Element const> | Set only when matching ::slotted() |
part_owning_parent | GC::Ptr<Element const> | Set only when matching ::part() |
collect_per_element_selector_involvement_metadata | bool | Enables setting flags on elements for targeted style invalidation |
attempted_pseudo_class_matches | PseudoClassBitmap | Tracks which pseudo-classes were evaluated (for invalidation) |
has_result_cache | HasResultCache* | Optional cache for :has() match results |
Sources: Libraries/LibWeb/CSS/SelectorEngine.h46-54
Diagram: High-level matching flow in SelectorEngine::matches
Sources: Libraries/LibWeb/CSS/SelectorEngine.cpp38-800
matches_attribute() (and the helper matches_single_attribute()) handles the full set of CSS attribute selector match types:
MatchType | CSS syntax | Behavior |
|---|---|---|
HasAttribute | [attr] | Attribute exists |
ExactValueMatch | [attr=val] | Exact match |
ContainsWord | [attr~=val] | Space-separated word match |
ContainsString | [attr*=val] | Substring match |
StartsWithSegment | [attr|=val] | Equal to val or starts with val- |
StartsWithString | [attr^=val] | Starts with val |
EndsWithString | [attr$=val] | Ends with val |
Case sensitivity is determined by CaseType (DefaultMatch, CaseSensitiveMatch, CaseInsensitiveMatch). DefaultMatch applies the HTML spec rule that certain presentational attributes on HTML elements in HTML documents are case-insensitive.
Sources: Libraries/LibWeb/CSS/SelectorEngine.cpp272-428
matches_pseudo_class() is a large switch on PseudoClass enum values. Selected cases:
| Pseudo-class | Implementation note |
|---|---|
:link, :any-link | element.matches_link_pseudo_class() |
:hover | matches_hover_pseudo_class() — checks hovered node and shadow-inclusive ancestors |
:focus-within | Checks if any descendant of element is the focused area |
:nth-child(), :nth-last-child() | Counts siblings matching optional of <selector> list; uses ANPlusBPattern::matches(index) |
:nth-of-type(), :nth-last-of-type() | Counts siblings with same tag name |
:has() | Delegates to matches_has_pseudo_class(), which calls matches_relative_selector(). Cannot be nested. |
:is(), :where() | Iterates argument_selector_list, returns true on first match |
:not() | Iterates argument_selector_list, returns false on first match |
:host | matches_host_pseudo_class() — matches only the shadow host from within a shadow tree |
:lang() | matches_lang_pseudo_class() — RFC 4647 extended filtering |
:indeterminate | Checks HTMLInputElement::indeterminate or HTMLProgressElement with no value attribute |
:read-write | matches_read_write_pseudo_class() — checks input/textarea/contenteditable |
:open | matches_open_state_pseudo_class() — details, dialog, select, input with picker |
Sources: Libraries/LibWeb/CSS/SelectorEngine.cpp532-750
:has() — The Relational Pseudo-Class:has() is the most complex pseudo-class because it requires forward tree traversal (from the anchor element downward) rather than the normal upward traversal.
matches_pseudo_class(Has, element, ...)
└─ matches_has_pseudo_class(selector, element, ...) [checks cache]
└─ matches_relative_selector(selector, 0, element, ...)
matches_relative_selector() handles the combinators inside the relative selector argument:
Combinator inside :has() | Behavior |
|---|---|
Descendant | for_each_in_subtree() looking for a matching descendant |
ImmediateChild | for_each_child() |
NextSibling | next_element_sibling() |
SubsequentSibling | Walk all subsequent siblings |
When collect_per_element_selector_involvement_metadata is enabled, elements traversed during :has() sibling combinators set a flag (set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator) for targeted invalidation.
HasResultCacheHasResultCache is a HashMap<HasResultCacheKey, HasMatchResult> keyed by (CSS::Selector*, DOM::Element*). When the engine confirms a positive :has() match and climbs ancestors, it also pre-populates cache entries for those ancestors (they all have the same matching descendant).
Sources: Libraries/LibWeb/CSS/SelectorEngine.cpp145-236 Libraries/LibWeb/CSS/SelectorEngine.h20-44
traverse_up()The internal traversal helper traverse_up() normally calls node->parent(). When a non-null shadow_host is provided, it switches to parent_or_shadow_host_element() and stops returning nodes once it has reached the shadow host itself. This confines descendant and child combinator matching to within the shadow tree.
Sources: Libraries/LibWeb/CSS/SelectorEngine.cpp42-58
should_block_shadow_host_matching()When the element being tested is the shadow host and the traversal is happening from within the shadow tree, most simple selectors are blocked from matching the host. Only the following are permitted:
PseudoClass::HostPseudoClass::Has, PseudoClass::Is, PseudoClass::Where (may contain :host in arguments)SimpleSelector::Type::Nesting (may nest :host)SimpleSelector::Type::PseudoElementSources: Libraries/LibWeb/CSS/SelectorEngine.cpp448-470
::slotted() and ::part()These pseudo-elements rely on MatchContext fields set by the style computer before calling matches():
slotted_element — the actual element assigned to the slot; the compound selector inside ::slotted() is matched against this element.part_owning_parent — the parent element that owns the shadow tree containing the part; used to verify the exportparts chain.Selector::collect_ancestor_hashes() (called from the Selector constructor) walks the compound selectors left of the subject compound and extracts up to 8 hashes from Id, Class, TagName, and Attribute simple selectors on the ancestor axis.
The style computer (StyleComputer) maintains a Bloom filter of all tokens present in the ancestor chain of the current element. Before running matches(), it checks whether all of the selector's ancestor hashes appear in the filter (can_use_ancestor_filter() + ancestor_hashes()). If any hash is absent, the selector cannot possibly match, and matches() is skipped entirely.
Sibling combinators (+, ~) break the ancestor axis; for those, the style computer falls back to counting shared ancestors above the sibling boundary.
Sources: Libraries/LibWeb/CSS/Selector.cpp131-218
can_use_fast_matches() returns true if the selector:
None, Descendant, or ImmediateChild combinatorsActive, AnyLink, Checked, Disabled, Empty, Enabled, FirstChild, Focus, FocusVisible, FocusWithin, Hover, LastChild, Link, LocalLink, OnlyChild, Root, State, Unchecked, Visited)Nesting, PseudoElement, or other complex typesWhen can_use_fast_matches() is true, the style computer can use a simplified matching path that skips allocation of a full MatchContext.
Sources: Libraries/LibWeb/CSS/Selector.cpp29-74
Selector::specificity() is computed lazily and cached in m_specificity. It packs three 8-bit counters into a u32:
Special rules:
:is(), :not(), :has() — specificity of most-specific argument selector:nth-child(An+B of S), :nth-last-child(An+B of S) — one pseudo-class plus specificity of most-specific S argument:where() — contributes zero to all three counters& (nesting) — zero if no parent selector exists; otherwise treated as :is(parent-selector-list)Sources: Libraries/LibWeb/CSS/Selector.cpp220-331
Default.css)Libraries/LibWeb/CSS/Default.css is the built-in user-agent stylesheet applied to every document. It is a regular CSS file that is loaded and parsed at startup by StyleComputer.
It is split into two sections:
<input type=range> / <meter> / <progress> appearances, non-standard keywords like -libweb-center.The stylesheet uses several pseudo-classes that are LibWeb-specific or from upcoming drafts:
| Pseudo-class | Used for |
|---|---|
:optimal-value | <meter> value in optimal range |
:suboptimal-value | <meter> value in suboptimal range |
:even-less-good-value | <meter> value in poor range |
:heading | Any heading element (h1–h6) |
:heading(N) | Heading elements at a specific level |
:popover-open | Elements with the popover attribute that are open |
These are defined in PseudoClasses.json and implemented in matches_pseudo_class().
Sources: Libraries/LibWeb/CSS/Default.css1-860 Libraries/LibWeb/CSS/SelectorEngine.cpp529-650
Libraries/LibWeb/Dump.cpp provides dump_selector(StringBuilder&, CSS::Selector const&, int indent_levels), which recursively prints the full structure of a Selector including combinators, simple selector types, attribute match types, pseudo-class names (via pseudo_class_name()), and nested argument selector lists.
Sources: Libraries/LibWeb/Dump.cpp429-620 Libraries/LibWeb/Dump.h30-31
Refresh this wiki