Skip to content

feat: property names from anchor positioning, scroll animations and view transitions - #64

Draft
Kristofer Baxter (kristofer-baxter) wants to merge 6 commits into
microsoft:mainfrom
kristofer-baxter:oss/properties
Draft

feat: property names from anchor positioning, scroll animations and view transitions#64
Kristofer Baxter (kristofer-baxter) wants to merge 6 commits into
microsoft:mainfrom
kristofer-baxter:oss/properties

Conversation

@kristofer-baxter

@kristofer-baxter Kristofer Baxter (kristofer-baxter) commented Sep 2, 2026

Copy link
Copy Markdown

6 of 6, and a draft until #63 merges.

A pull request from a fork can only be based on main, so the diff here includes every commit beneath this one in the stack. The commit this pull request adds is c29d83c, the last one on the branch. Once #63 merges the diff will reduce to that commit alone, and I will mark this ready.

Part of the split of #57.

The supported property name list stops at the properties that were standard when it was written, so a stylesheet using anchor positioning, scroll-driven animations or view transitions gets no property name scope for the properties those specifications add. This adds them, along with the other property names that have since reached a specification: field-sizing, interpolate-size, content-visibility, the contain-intrinsic-* family, the text-box* and text-wrap* families, margin-trim, reading-flow, reading-order, corner-shape with its physical and logical longhands, dynamic-range-limit and the MathML Core layout properties.

Each family is added whole rather than in part, so animation-timeline arrives with animation-range and its two longhands, scroll-marker-group with scroll-target-group, and forced-color-adjust with print-color-adjust.

Every name added here is asserted, so that removing one from the grammar fails a test. The declaration each assertion uses is written with inherit, which every property takes, rather than a value the property's own grammar may not accept.

Adds scopes for `@container`: the container name, size features, logical
operators, `style()` queries and `scroll-state()` queries, including
grouped and boolean forms. The body carries
`meta.at-rule.container.body.css` and both of its braces are scoped,
matching the other block at-rules.

A query in parentheses may itself be a parenthesised query, so the
groups nest rather than close at the first bracket, and
`<general-enclosed>` is parsed so a bracket inside a string is not read
as one. A `<style-query>` is parsed recursively, so `not`, grouped
queries and ranges are not read as a declaration list. A feature value
is a component value, so a function may stand in for a keyword.

`style()` holds a `<declaration-value>`, where a `;` nested in brackets
or a balanced block is part of the value. Its declaration-value region
therefore ends only at `}` or `)`, so `style(--x: [a;b])` keeps the
whole value.

The container name is a `<custom-ident>`, so it may begin with an
escape. Only `none`, `and`, `not` and `or` are reserved, so `style` and
`scroll-state` are names in their own right and only a bracket straight
after either one makes it a query function. The query is optional, so a
name may run into the body, and the prelude is a comma-separated list,
so a name may start at a comma. The
cost of a prelude is linear in its length: doubling a 12.5 KB prelude
twice roughly doubles the time each step, and 50 KB takes under half a
second, which is far past any real stylesheet.

An unknown query function is `<general-enclosed>` wherever a query is
valid, not only inside parentheses. A `)` separates the identifier that
follows it, so `)and(` needs no whitespace. A style range may be written
without spaces, value first, or chained, and its custom property name
stops at the comparison.

A size container feature takes a length or a ratio, and only
`orientation` takes a keyword, so the values `@media` accepts are not
query values here: `@container (width: fullscreen)` reads `fullscreen`
as no more than an identifier.

A malformed prelude leaks its header scope to the rest of the file,
which is what `@media` already does. `@container` has no semicolon
form, so `@container nonsense;` leaks with the rest of them.
Adds scopes for `@property` descriptors and its custom property name,
`@scope` preludes as selectors with the scope root and the scoping limit
distinguished, and the `@starting-style` body. Block at-rules whose
prelude is omitted entirely open their body correctly.

A `@scope` prelude holds a selector list, so its parentheses balance.
`@scope (:host(.a)) to (.b)` scopes as a selector rather than closing
the prelude at the inner bracket.

The scoping limit keyword is an identifier, so `t\6f` and `\t\o` are
both recognised as `to`. A hexadecimal escape is at most six digits, so
`\00000074o` is not. A custom property name ending in a hexadecimal
escape consumes the whitespace that terminates it, so the name is not
cut short.

`@scope` takes a declaration written straight into its body, without a
selector around it, and so does a `@starting-style` nested inside a
style rule, so a declaration in either position scopes as one. A
top-level `@starting-style` holds style rules, and the test uses the
nested form for that reason.

An unclosed bracket in a `@scope` prelude, as in `@scope (:host(.a){`,
carries the prelude scope further than `main` does. It leaks rather than
recovering, which is the direction this grammar wants, but it is a
difference and worth knowing about.
Adds scopes for `env()`, where only the first argument is the
environment variable name and indices, fallbacks, comments and
continuation lines are handled; `anchor()` and `anchor-size()` with
their keywords; `color-mix()`, `light-dark()`, `contrast-color()` and
`device-cmyk()` as color functions, including custom color spaces and
interpolation keywords; and the math and timeline functions the
css-values-5 draft defines.

An `env()` whose first argument cannot be a name, such as
`env(calc(x) red)`, `env(-10px)` or `env(initial)`, is left alone rather
than having a later identifier read as the name, on that line or on a
following one. An environment variable name, an anchor name and a custom
color space are all identifiers, so any of them may contain an escape,
may begin with a digit after the two hyphens, and is not read out of the
middle of a longer identifier.

`anchor()` takes an `<anchor-side>` and `anchor-size()` takes an
`<anchor-size>`. Those two sets are disjoint, so they are matched
separately and neither function takes the other's keywords.

`color-mix()` is the only color function that takes a
`<color-interpolation-method>`, so the interpolation-keyword matcher is
on it alone. The other color functions this change adds join the
existing color rule, and `rgb()` and its neighbours tokenize exactly as
before.

Every function and keyword name this change adds is asserted, so that
removing one from the grammar fails a test.
Adds scopes for `::part()`, `::slotted()`, functional `:host()`,
`:state()`, the scroll marker position pseudo-classes and the other
pseudo-classes and pseudo-elements Selectors 4 defines, each parsed with
the argument grammar its arguments call for. It also adds the names
Selectors 4 defers to Level 5 and Level 5 goes on to define: `:blank`,
`:local-link`, `:current`, `:past` and `:future`. `::column` comes from
css-multicol-2 rather than Selectors 5, and is added here as well.

`:nth-child()` gains the `of <selector>` clause, and accepts a universal
or namespaced selector after `of`. It is split from `:nth-of-type()`,
which takes no such clause, so `of` is no longer accepted there.

`::slotted()`, `:host()` and `:host-context()` each take a single
compound selector, and `:current()` takes a list of them, so a
combinator in any of the four is marked invalid, as is a comma in the
first three. That includes the descendant combinator, which is written
as whitespace. Whitespace that terminates a hexadecimal escape belongs
to the name before it, so `:host(.\61 .b)` is one compound selector and
stays valid, while the second space in `:host(.\61  .b)` is a
combinator, and so is the space in `:host(.\\61 .b)`, where the
backslash is itself escaped and the digits are literal text. Past two
such runs the whitespace is left alone rather than called a combinator,
since missing one reads better than calling valid CSS invalid. `:is()`,
`:not()`, `:has()` and `:where()` take complex selectors and are
unaffected.

Each argument is matched to the production the specification gives it.
`::part()` takes `<ident>+`, so every name in it is an argument.
`:state()` takes one `<ident>`, `::highlight()` one `<custom-ident>`,
`::scroll-button()` one direction, and a view-transition pseudo-element
one `<pt-name-selector>`, so in those a second argument is not scoped as
one. An identifier may begin with an escape, and it has to begin the way
css-syntax-3 says: a lone hyphen, a digit after one hyphen, and a
leading digit are not identifiers. A `<custom-ident>` excludes the
CSS-wide keywords and `default`, so `::highlight(default)` is not a
name, while `:state(default)` is, since that argument is an `<ident>`.

`:local-link()` is here in its functional form too, taking one
non-negative integer.

A keyword list is written out rather than parsed, so a keyword spelled
as an escape is not recognised: `::scroll-button(\75 p)` and
`:nth-child(2n \6f f a)` leave the argument alone. Nothing is scoped
wrongly, and `main` reads neither of them either.

Enforcing a sole argument means anchoring the match to the position
after the opening parenthesis. That anchor does not survive a line
break, so each of those matches takes a line start as well, and a
selector wrapped across lines keeps its argument scope.

The class selector additions css-view-transitions-2 makes to
`::view-transition-group()`, `::view-transition-image-pair()`,
`::view-transition-old()` and `::view-transition-new()` are not
implemented. A class in that position keeps tokenizing as an ordinary
class selector, as it does on `main`.

Every pseudo-class and pseudo-element this change adds is asserted, so
that removing one from the grammar fails a test. The scope names and the
brackets of each new functional selector are asserted too.
The media feature list stops at the features that were standard when it
was written, so `@media (any-pointer: coarse)` and
`@media (prefers-reduced-motion: reduce)` get no feature name scope and
no value scope. This adds the interaction features, the user-preference
features and the rest of what Media Queries 5 defines, and adds the
viewport segment features as range features, since they take a count
rather than a keyword.

Each feature name is added together with the values Media Queries 5
defines for it, so that a name never arrives without its values. The
value list is flat, as it already was, so a value is recognised in any
feature rather than only in the one that defines it.

`video-width`, `video-height` and `video-resolution` are deliberately
absent. Media Queries 5 dropped all three, so the set here is the set
the current draft still defines.

Every feature name and every feature value added here is asserted, so
that removing one from the grammar fails a test.
…iew transitions

The supported property name list stops at the properties that were
standard when it was written, so a stylesheet using anchor positioning,
scroll-driven animations or view transitions gets no property name
scope for the properties those specifications add. This adds them,
along with the other property names that have since reached a
specification: `field-sizing`, `interpolate-size`, `content-visibility`,
the `contain-intrinsic-*` family, the `text-box*` and `text-wrap*`
families, `margin-trim`, `reading-flow`, `reading-order`,
`corner-shape` with its physical and logical longhands,
`dynamic-range-limit` and the MathML Core layout properties.

Each family is added whole rather than in part, so `animation-timeline`
arrives with `animation-range` and its two longhands,
`scroll-marker-group` with `scroll-target-group`, and
`forced-color-adjust` with `print-color-adjust`.

Every name added here is asserted, so that removing one from the
grammar fails a test. The declaration each assertion uses is written
with `inherit`, which every property takes, rather than a value the
property's own grammar may not accept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants