Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Selectors, values, and at-rules

Selectors

Matching is delegated to the selectors crate from Servo, so CSS3 selectors mostly work as they are.

SelectorsSupportedNotes
Type (p) and universal (*)
Class (.foo) and ID (#foo)
Attribute ([a], [a=v], [a^=v], [a$=v], [a*=v], [a~=v], [a|=v])The case-insensitive i flag works too
Descendant (whitespace) and child (>)
Adjacent sibling (+) and general sibling (~)
Selector lists (,)
Nesting (.a { .b { } }, & .b, &.b, > .b)CSS Nesting. A selector without & is treated as & .b, and one starting with a combinator as & > .b. & is replaced by :is(parent), so the specificity becomes that of the most specific parent selector. Declarations written after a nested rule join the cascade after that rule. Nested at-rules such as @media are not supported (the whole block is ignored)
Namespaces (ns|E)@namespace itself is not supported

Pseudo-classes

Pseudo-classesSupportedNotes
:root
:first-child, :last-child, :only-child
:nth-child(), :nth-last-child()In streaming mode :nth-last-child() gives a different result; see below
:first-of-type, :last-of-type, :only-of-type, :nth-of-type(), :nth-last-of-type()In streaming mode every one of these except :first-of-type and :nth-of-type() gives a different result; see below
:empty
:not()
:is(), :where()The argument list is forgiving: an unsupported selector among the arguments drops only that argument. For specificity, :is() counts as its most specific argument and :where() always counts as zero
:has()Descendant, >, + and ~ may all be used. Specificity follows the same rule as :is(). Nesting (a :has() inside a :has()) and pseudo-elements are not allowed, per the spec. In streaming mode :has(~ ...) never matches (see below)
:hover, :active, :focus, :focus-within, :focus-visible, :target, :enabled, :disabled, :checked, :visited⚠️They parse but never match, since a static PDF has no interaction state for them to describe
:link, :any-linkMatches an <a> that has an href

A selector containing an unsupported pseudo-element, such as ::first-line, causes the whole rule to be dropped. Ones that parse, such as :hover, survive as rules but never match. The argument list of :is() and :where() is the one exception: an unsupported argument is dropped on its own and the rule survives.

Pseudo-elements

Pseudo-elementsSupportedNotes
::before, ::after⚠️Generated text through content only. It is drawn with the computed style of the host element and has no box style of its own, so margin, padding, display, and the like do not apply. Nothing is generated on an element that has block children
::first-letter⚠️Only font-family, font-size, font-weight, font-style, color, text-decoration-line, and text-transform can be overridden; float and the box model properties are not supported
::first-lineA parse error
::marker, ::selection, ::placeholderA parse error

Values, units, and functions

Lengths

UnitSupported
px, em, rem
mm, cm, in, pt, pc, Q
%, on properties that take a percentage
A bare 0
ex, ch, vw, vh, vmin, vmax, lh

Absolute units are read with one inch as 96px, so 10mm is 37.795px. Physical dimensions can be written directly for print, which means @page { size: 210mm 297mm; margin: 15mm; } works as written.

As an exception, size in @page also accepts page size keywords such as a4 and letter, and landscape or portrait.

Angles, used only by transform

deg, rad, grad, and turn are supported, as is a bare 0 ✅.

Function

FunctionSupportedNotes
calc()⚠️+, -, *, /, and nesting with parentheses or calc(). The terms may be lengths, whether absolute units or em and rem, percentages, and numbers. It works on any property that takes a length or a percentage
min(), max(), clamp()
var()⚠️Custom properties (--foo) are resolved by text substitution before parsing. Fallbacks such as var(--x, 10px) and references between custom properties work. Unlike the specification, this does not follow the cascade or inheritance; it is a simple resolution in which the last declaration in the document wins
url()In background-image, list-style-image, the src of @font-face, and @import. Relative URLs are resolved against <base href> or the location of the input
attr()⚠️Only inside content
counter(), counters()Inside content. The style in the second argument comes from the list-style-type values
Gradients such as linear-gradient()
env(), image-set(), element()

Colours

NotationSupported
Named colours, the CSS colour keywords such as red
#rgb, #rgba, #rrggbb, #rrggbbaa
rgb(), rgba(), both comma separated and space separated
hsl(), hsla(), hwb()
lab(), lch(), oklab(), oklch()✅ (converted to sRGB for drawing)
currentcolor, transparent
color(), such as color(display-p3 ...)
color-mix()⚠️ (see below)
The relative colour syntax rgb(from ...)

Colours with alpha are drawn through a PDF ExtGState, for both fills and backgrounds.

color-mix()

color-mix(in <colorspace> [<hue-interpolation-method> hue]?, <color> <percentage>?, <color> <percentage>?) is supported. Weights are normalised as the spec requires — over 100% only the ratio matters, under 100% the result becomes transparent by the shortfall — and alpha is premultiplied before interpolating.

  • Colour spaces: srgb, srgb-linear, lab, oklab, xyz (xyz-d65), hsl, hwb, lch and oklch
  • Hue interpolation: shorter (the default), longer, increasing and decreasing
  • It can be nested (up to 16 levels)

The following are not supported; using one drops that declaration alone.

  • display-p3, a98-rgb, prophoto-rgb and rec2020. The output is PDF DeviceRGB, so accepting them would only round the result back to sRGB and the request would not mean what it says
  • currentcolor as an operand. currentcolor is resolved after the cascade, once the element’s own color is known, whereas the mixing happens at parse time, so the value is not available yet
  • xyz-d50 (it would need a white point conversion)

At-rules

At-rulesSupportedNotes
@media⚠️Only the media type is evaluated. A screen block, and any negation other than not screen, is ignored entirely, while print, all, and a missing type are applied. Feature queries such as (min-width: ...) are skipped without being evaluated, so the contents apply as long as the type matches
@page⚠️size, taking a keyword, one or two <length> values, or landscape and portrait, plus the margin properties. The pseudo-classes :first, :left, and :right are supported on their own; named pages such as @page intro, :blank, and compound pseudo-classes such as :first:left are not
The margin boxes inside @page⚠️All sixteen of them, @top-left-corner, @top-left, @top-center, @top-right, and so on. Only text from content is drawn; decoration such as background colours and borders is not supported. Page numbers can be printed with counter(page) and counter(pages), though counter(pages) is unavailable in streaming mode
@font-face⚠️The font-family, src, unicode-range, font-weight, and font-style descriptors are supported, including local() and url() with format() or tech() inside src. Other descriptors such as font-display are ignored. Font files may be TTF or OTF only; WOFF and WOFF2 are not supported
@importNesting is supported to a depth of 16, beyond which the offending import alone is ignored, and cycles are detected. A media condition such as @import url(...) screen; is not evaluated; the file is always imported
@charsetThe input is assumed to be UTF-8
@layer⚠️Rules inside the block are hoisted to the top level in source order (@layer a { .x {} } is the same as .x {}). Layer precedence is not implemented; the usual cascade (specificity, last wins) decides. The @layer a, b; ordering statement is ignored. A stylesheet that wraps its whole output in @layer, as Tailwind v4 does, can be passed as is
@supports, @keyframes, @namespace, @counter-style, @container, @propertyThe whole block is ignored

Restrictions specific to streaming mode

In Mode::Batch the whole DOM is available, so none of this applies. The following holds only in Mode::Streaming.

A top-level element (a direct child of <body>) is treated as settled once its next sibling appears. Only selectors that need to know whether more elements of the same kind follow therefore give a different result, and only for those top-level elements — inside one of them the subtree is complete, so nothing changes.

  • :last-of-type, :only-of-type, :nth-last-child(), :nth-last-of-type() and :has(~ ...) match too much or miss altogether. Using any of them prints a warning
  • :last-child, :empty, and :has() over descendants or the immediately next sibling, on the other hand, line up with what settling already guarantees, so they give the same result as batch mode
  • +, ~, :first-child, :nth-child(), :first-of-type, :nth-of-type() and :only-child also give the same result as batch mode. In a document that uses any of them, a processed top-level element has only its descendants released, so the element itself stays visible as a sibling (what remains is one node per top-level element, so almost as much is still freed)
  • A <style> tag after <body> starts is an error and returns EngineError::UnsupportedInStreamingMode. Keep all <style> in <head>, so that the layout does not quietly fall apart
  • position: absolute and fixed are ignored
  • counter(pages), the total page count, is unavailable