Selectors and the cascade

The selector engine implements standard CSS selectors with spec matching semantics, plus ::before/::after generated content. Specificity, source order, and inline-style precedence follow the cascade; inherit, initial, and unset work.

Simple selectors and combinators

  • Type (div), class (.card), id (#main), universal (*), and selector lists (h1, h2).
  • Combinators: descendant (nav a), child (ul > li), adjacent sibling (h2 + p), general sibling (h2 ~ p).

Attribute selectors

All operators, quoted or unquoted:

FormMatches
[disabled]attribute present
[type="text"]exact value
[href^="https"]prefix
[href$=".pdf"]suffix
[href*="docs"]substring
[data-tags~="beta"]whitespace-separated word
[lang\|="en"]exact or dash-prefixed (en-GB)

One renderer nuance: Svelte stringifies attribute values, so disabled={false} arrives as the string "false" — svelterm’s boolean attribute handling (:disabled, [open] behaviours, focus skipping) treats "false" as off.

Pseudo-classes

  • Structural: :root, :first-child, :last-child, :only-child, :empty (comments and empty text ignored, as in browsers), :first-of-type, :last-of-type, :only-of-type, and the full An+B family — :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-of-type() (odd, even, 3, 2n+1, -n+3…).
  • State: :focus (keyboard/mouse focus), :hover (mouse), :checked, :disabled, :enabled (form controls only, as in browsers).
  • Logical: :not(), :is(), :where() (zero specificity), each taking selector lists.
tr:nth-child(even)        { background: #16181d; }
input:checked             { color: green; }
button:disabled           { opacity: dim; }
details[open] > summary   { color: cyan; }

Pseudo-elements: ::before / ::after

Generated content renders as an inline box at the start/end of the element (legacy single-colon syntax accepted):

a[href$=".pdf"]::after { content: " [pdf]"; color: red; }
.badge::before         { content: attr(data-count) " "; font-weight: bold; }

content: supports quoted strings, attr(x) against the host element, counter(name), space-separated concatenation, and none/"" (no box). Pseudo boxes are invisible to :empty and :nth-*, and style like inline elements (they inherit the host’s visual attributes unless the pseudo rule overrides them). In table-internal boxes they follow CSS anonymous-box rules: a ::before on a row renders as a leading anonymous cell.

Counters

counter-reset and counter-increment work with optional amounts:

.doc     { counter-reset: sec; }
.section { counter-increment: sec; }
.section::before { content: counter(sec) ". "; }

Counters resolve in document order with a flat namespace — nested elements share the same counter rather than creating a scoped one, and counters() (the nested-join form) is not supported. Values update on full style resolution, so an incremental restyle can briefly show stale numbers.

What re-resolves when

Selector matching is live: any attribute change (including inline style, checked, open, data-*) re-resolves the element and its descendants, so state-dependent rules like details[open] > * or [data-status="error"] update as your component mutates.