feat: Media Queries 5 interaction and user-preference features - #63
Draft
Kristofer Baxter (kristofer-baxter) wants to merge 5 commits into
Draft
feat: Media Queries 5 interaction and user-preference features#63Kristofer Baxter (kristofer-baxter) wants to merge 5 commits into
Kristofer Baxter (kristofer-baxter) wants to merge 5 commits into
Conversation
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.
Kristofer Baxter (kristofer-baxter)
force-pushed
the
oss/media-features
branch
from
September 2, 2026 01:00
4009532 to
fda961b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
5 of 6, and a draft until #62 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 isfda961b, the last one on the branch. Once #62 merges the diff will reduce to that commit alone, and I will mark this ready.Part of the split of #57.
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. The viewport segment features are added 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-heightandvideo-resolutionare 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.