Skip to content

CSS resolver: cascade and specificity

The CssResolver matches selectors against the token stream, orders matched rules by cascade layer, specificity, and document order, then applies !important in a second pass.

Terminal window
composer require nextpdf/core:^3

CssResolver is the Layer 1 component (per ADR-010). It owns the parsed Cascading Style Sheets (CSS) rules and decides which declarations apply to each element. The class was extracted from HtmlParser to keep the structure clear, and it is internal rather than a public application programming interface (API).

The resolver does not need a document tree. Selector matching reads the flat token stream and uses the index maps that HtmlChildScanner builds in pipeline Stage 3: child counts, same-tag counts, and emptiness. Those maps answer structural pseudo-classes. The relational :has() selector uses the bounded pre-scan described in streaming constraints.

Cascade resolution runs in two passes inside CssResolver::resolveMatchingProperties(). Pass 1 applies normal declarations in cascade order: cascade-layer weight first, then specificity, then document order. Pass 2 applies !important declarations in specificity order. An !important declaration overrides any normal declaration, regardless of specificity. This two-pass split is the implementation strategy, and it produces the resolved property set that the layout layer consumes.

The cascade order the resolver implements aligns with the World Wide Web Consortium (W3C) CSS Cascading and Inheritance specification. Declarations are sorted first by origin and importance, then by selector specificity. For equal specificity, the last declaration in document order wins (CSS Cascade 5 §6.4; see Conformance). The in-source comment in CssResolver cites the same clause, so you have a third way to verify this behavior, alongside the specification and the glossary.

Specificity is computed as an (A, B, C) triple from counts of ID, class, and type components, and the triples are compared component by component (Selectors Level 4 §16). NextPDF computes specificity for each matched rule before it sorts the cascade.

One constraint matters. The §6.4.3 layer-inversion rule applies to !important declarations across cascade layers, and the source records it as outstanding for the cascade-layer work cluster. When cascade layers are declared and !important crosses layers, the resolved order can differ from full specification behavior. The CSS support matrix is the authority for per-feature support state, and this page does not restate per-property support.

SymbolLocationRole
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): voidsrc/Html/CssResolver.phpParse a <style> block into rules.
CssResolver::resolveMatchingProperties(...)src/Html/CssResolver.phpMatch selectors and resolve the two-pass cascade.
CssResolver::resolveHasSelectors(array $tokens): arraysrc/Html/CssResolver.phpBounded :has() pre-scan (gated).
CssResolver::resolveFirstLetterProperties(...)src/Html/CssResolver.phpResolve ::first-letter properties.
CssResolver::resolvePseudoElementProperties(...)src/Html/CssResolver.phpResolve ::before / ::after properties.
CssResolver::getLayerRegistry(): LayerRegistrysrc/Html/CssResolver.phpDeclared cascade layers.

You do not call the resolver directly. You write CSS, and the resolver runs inside writeHtml(). In the cascade below, p resolves to red because the class rule has higher specificity than the type rule.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->writeHtml(
'<style>p { color: blue; } .lead { color: red; }</style>'
. '<p class="lead">Higher-specificity class wins.</p>'
);
$doc->save(__DIR__ . '/output/cascade.pdf');

This example shows the !important second pass. The !important type declaration overrides the inline-equivalent class declaration, even though the class selector has higher specificity.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->writeHtml(
'<style>p { color: green !important; } .lead { color: red; }</style>'
. '<p class="lead">!important overrides higher specificity.</p>'
);
$doc->save(__DIR__ . '/output/important.pdf');
  • !important ignores specificity. Pass 2 applies !important declarations in specificity order, and those declarations always override normal declarations.
  • Cascade layers + !important across layers. The source records the §6.4.3 layer-inversion rule for important declarations as outstanding. Verify behavior against the CSS support matrix before relying on it.
  • No layers declared is the fast path. With no @layer, ordering reduces to specificity-only behavior and is bit-identical to pre-layer behavior.
  • :has() is gated. The relational pre-scan runs only when the css.has experimental feature is enabled.
  • Selector matching is stream-based. Structural selectors use index maps, not a tree walk. A selector that would need arbitrary tree navigation beyond the index maps is not resolvable in this model.

In the worst case, selector matching is O(rules × elements), bounded by the streaming caps. The two cascade sorts are O(matched rules · log matched rules) per element. The unlayered path skips layer resolution entirely. The per-page performance_budget (wall_ms: 1500, peak_mb: 64) sets the operational ceiling. The HTML render-pipeline benchmark guards regressions (merged work, PR #564).

The resolver sees only CSS that DefaultHtmlSecurityPolicy::isCssPropertyAllowed() admits. The allowlist sets the security ceiling, and the runtime support table sets a separate capability ceiling. A policy-blocked property never reaches the cascade. See the HTML module security model.

BehaviorSpecificationClausereference_id
Cascade sort: origin/importance → specificity → order of appearanceW3C CSS Cascading and Inheritance Level 5§6.4 (css_cascade_5#x1.x7.x1.p21)
Specificity as an (A,B,C) triple from ID/class/type countsW3C Selectors Level 4§16 (selectors_4#x1.x16.p2)
Deterministic parsing and parse-error recoveryW3C CSS Syntax Level 3§4 (css_syntax_3#x1.x4.p2)

W3C material is CC-BY 4.0. The claims above are paraphrased. Clause and chunk identifiers are provided for verification. NextPDF does not claim full conformance to these modules; see the CSS support matrix for verified per-module status.

Enterprise capability. Premium widens the matched and applied property set. The cascade algorithm and the two-pass !important model are identical across editions. See the CSS support matrix.