Selectors, values, and at-rules
- Selectors
- Pseudo-classes
- Pseudo-elements
- Values, units, and functions
- Colours
- At-rules
- Restrictions specific to streaming mode
Selectors
Matching is delegated to the selectors crate from Servo, so CSS3 selectors mostly work as they are.
| Selectors | Supported | Notes |
|---|---|---|
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 (,) | ✅ | |
Namespaces (ns|E) | ❌ | @namespace itself is not supported |
Pseudo-classes
| Pseudo-classes | Supported | Notes |
|---|---|---|
:root | ✅ | |
:first-child, :last-child, :only-child | ✅ | In streaming mode :last-child never matches; see below |
:nth-child(), :nth-last-child() | ✅ | As above; :nth-last-child() never matches in streaming mode |
:first-of-type, :last-of-type, :only-of-type, :nth-of-type(), :nth-last-of-type() | ✅ | As above |
:empty | ✅ | As above |
:not() | ✅ | |
:is(), :where(), :has() | ❌ | A parse error; the whole selector is ignored |
: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-link | ✅ | Matches an <a> that has an href |
A selector containing an unsupported pseudo-class, such as :is(), causes the whole rule to be dropped. Ones that parse, such as :hover, survive as rules but never match.
Pseudo-elements
| Pseudo-elements | Supported | Notes |
|---|---|---|
::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-line | ❌ | A parse error |
::marker, ::selection, ::placeholder | ❌ | A parse error |
Values, units, and functions
Lengths
| Unit | Supported |
|---|---|
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
| Function | Supported | Notes |
|---|---|---|
calc() | ⚠️ | +, -, *, /, and nested parentheses. 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
| Notation | Supported |
|---|---|
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(), and the relative colour syntax rgb(from ...) | ❌ |
Colours with alpha are drawn through a PDF ExtGState, for both fills and backgrounds.
At-rules
| At-rules | Supported | Notes |
|---|---|---|
@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 |
@import | ✅ | Nesting 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 |
@charset | ❌ | The input is assumed to be UTF-8 |
@supports, @keyframes, @namespace, @counter-style, @layer, @container, @property | ❌ | The 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.
- Selectors that look backwards never match:
:last-child,:last-of-type,:nth-last-child(),:nth-last-of-type(), and:empty. They cannot be decided until the parent’s child list is complete - A
<style>tag after<body>starts is an error and returnsEngineError::UnsupportedInStreamingMode. Keep all<style>in<head>, so that the layout does not quietly fall apart position: absoluteandfixedare ignoredcounter(pages), the total page count, is unavailable