CSSWG Minutes Telecon (2024-08-21)

View Transitions are one of the most awesome features CSS has shipped in recent times. Its title is self-explanatory: transitions between views are possible with just CSS, even across pages of the same origin! What’s more interesting is its subtext, since there is no need to create complex SPA with routing just to get those eye-catching transitions between pages.

What also makes View Transitions amazing is how quickly it has gone from its first public draft back in October 2022 to shipping in browsers and even in some production contexts like Airbnb — something that doesn’t happen to every feature coming to CSS, so it shows how rightfully hyped it is.

That said, the API is still new, so it’s bound to have some edge cases or bugs being solved as they come. An interesting way to keep up with the latest developments about CSS features like View Transitions is directly from the CSS Telecom Minutes (you can subscribe to them at W3C.org).


View Transitions were the primary focus at the August 21 meeting, which had a long agenda to address. It started with a light bug in Chrome regarding the navigation descriptor, used in every cross-document view transition to opt-in to a view transition.

@view-transition {
  navigation: auto | none;
}

Currently, the specs define navigation as an enum type (a set of predefined types), but Blink takes it as a CSSOMString (any string). While this initially was passed as a bug, it’s interesting to see the conversation it sparked on the GitHub Issue:

Actually I think this is debatable, we don’t currently have at rules that use enums in that way, and usually CSSOM doesn’t try to be fully type-safe in this way. e.g. if we add new navigation types and some browsers don’t support them, this would interpret them as invalid rules rather than rules with empty navigation.

The last statement may not look exciting, but it opens the possibility of new navigation types beyond auto and none, so think about what a different type of view transition could do.

And then onto the CSSWG Minutes:

emilio: Is it useful to differentiate between missing auto or none?

noamr: Yes, very important for forward compat. If one browser adds another type that others don’t have yet, then we want to see that there’s a difference between none or invalid

emilio: But then you get auto behavior?

noamr: No, the unknown value is not read for purpose of nav. It’s a vt role without navigation descriptor and no initial value Similar to having invalid rule

So in future implementations, an invalid navigation descriptor will be ignored, but exactly how is still under debate:

ntim: How is it different from navigation none?

noamr: Auto vs invalid and then auto vs none. None would supersede auto; it has a meaning to not do a nav while invalid is a no-op.

ntim: So none cancels the nav from the prev doc?

noamr: Yes

The none has the intent to cancel any view transitions from a previous document, while an invalid or empty string will be ignored. In the end, it resolved to return an empty string if it’s missing or invalid.

RESOLVED: navigation is a CSSOMString, it returns an empty string when navigation descriptor is missing or invalid

Onto the next item on the agenda. The discussion went into the view-transition-group property and whether it should have an order of precedence. Not to confuse with the pseudo-element of the same name (::view-transition-group) the view-transition-group property was resolved to be added somewhere in the future. As of right now, the tree of pseudo-elements created by view transitions is flattened:

::view-transition
├─ ::view-transition-group(name-1)
│  └─ ::view-transition-image-pair(name-1)
│     ├─ ::view-transition-old(name-1)
│     └─ ::view-transition-new(name-1)
├─ ::view-transition-group(name-2)
│  └─ ::view-transition-image-pair(name-2)
│     ├─ ::view-transition-old(name-2)
│     └─ ::view-transition-new(name-2)
│ /* and so one... */

However, we may want to nest transition groups into each other for more complex transitions, resulting in a tree with ::view-transition-group inside others ::view-transition-group, like the following:

::view-transition
├─ ::view-transition-group(container-a)
│  ├─ ::view-transition-group(name-1)
│  └─ ::view-transition-group(name-2)
└─ ::view-transition-group(container-b)
    ├─ ::view-transition-group(name-1)
    └─ ::view-transition-group(name-2)

So the view-transition-group property was born, or to be precise, it will be at some point in timer. It might look something close to the following syntax if I’m following along correctly:

view-transition-group: normal | <ident> | nearest | contain;
  • normal is contained by the root ::view-transition (current behavior).
  • <ident> will be contained by an element with a matching view-transition-name
  • nearest will be contained by its nearest ancestor with view-transition-name.
  • contain will contain all its descendants without changing the element’s position in the tree

The values seem simple, but they can conflict with each other. Imagine the following nested structure:

A  /* view-transition-name: foo */
└─ B /* view-transition-group: contain */
   └─ C /* view-transition-group: foo */

Here, B wants to contain C, but C explicitly says it wants to be contained by A. So, which wins?

vmpstr: Regarding nesting with view-transition-group, it takes keywords or ident. Contain says that all of the view-transition descendants are nested. Ident says same thing but also element itself will nest on the thing with that ident. Question is what happens if an element has a view-transition-group with a custom ident and also has an ancestor set to contain – where do we nest this? the contain one or the one with the ident? noam and I agree that ident should probably win, seems more specific.

<khush>: +1

The conversations continued if there should be a contain keyword that wins over <ident>

emilio: Agree that this seems desirable. Is there any use case for actually enforcing the containment? Do we need a strong contain? I don’t think so?

astearns: Somewhere along the line of adding a new keyword such as contain-idents?

<fantasai>: “contain-all”

emilio: Yeah, like sth to contain everything but needs a use case

But for now, it was set for <ident> to have more specificity than contain

PROPOSED RESOLUTION: idents take precedence over contain in view-transition-group

astearns: objections or concerns or questions?

<fantasai>: just as they do for <ident> values. (which also apply containment, but only to ‘normal’ elements)

RESOLVED: idents take precedence over contain in view-transition-group

Lastly, the main course of the discussion: whether or not some properties should be captured as styles instead of as a snapshot. Right now, view transitions work by taking a snapshot of the “old” view and transitioning to the “new” page. However, not everything is baked into the snapshot; some relevant properties are saved so they can be animated more carefully.

From the spec:

However, properties like mix-blend-mode which define how the element draws when it is embedded can’t be applied to its image. Such properties are applied to the element’s corresponding ::view-transition-group() pseudo-element, which is meant to generate a box equivalent to the element.

In short, some properties that depend on the element’s container are applied to the ::view-transition-group rather than ::view-transition-image-pair(). Since, in the future, we could nest groups inside groups, how we capture those properties has a lot more nuance.

noamr: Biggest issue we want to discuss today, how we capture and display nested components but also applies to non-nested view transition elements derived from the nested conversation. When we nest groups, some CSS properties that were previously not that important to capture are now very important because otherwise it looks broken. Two groups: tree effects like opacity, mask, clip-path, filters, perspective, these apply to entire tree; borders and border-radius because once you have a hierarchy of groups, and you have overflow then the overflow affects the origin where you draw the borders and shadows these also paint after backgrounds

noamr: We see three options.

  1. Change everything by default and don’t just capture snapshot but add more things that get captured as ?? instead of a flat snapshot (opacity, filter, transform, bg borders). Will change things because these styles are part of the group but have changed things before (but this is different as it changes observable computed style)
  2. Add new property view-transition-style or view-transition-capture-mode. Fan of the first as it reminds me of transform-style.
  3. To have this new property but give it auto value. If group contains other groups when you get the new mode so users using nesting get the new mode but can have a property to change the behavior If people want the old crossfade behavior they can always do so by regular DOM nesting

Regarding the first option about changing how all view transitions capture properties by default:

bramus: Yes, this would be breaking, but it would break in a good way. Regarding the name of the property, one of the values proposed is cross-fade, which is a value I wouldn’t recommend because authors can change the animation, e.g. to scale-up/ scale-down, etc. I would suggest a different name for the property, view-transition-capture-mode: flat | layered

Of course, changing how view transitions work is a dilemma to really think about:

noamr: There is some sentiment to 1 but I feel people need to think about this more?

astearns: Could resolve on option 1 and have blink try it out to see how much breakage there is and if its manageable then we’re good and come back to this. Would be resolving one 1 unless it’s not possible. I’d rather not define a new capture mode without a switch

…so the best course of action was to gather more data and decide:

khush: When we prototype we’ll find edge cases. We will take those back to the WG in that case. Want to get this right

noamr: It involves a lot of CSS props. Some of them are captured and not painted, while others are painted. The ones specifically would all be specified

After some more discussion, it was resolved to come back with compat data from browsers, you can read the full minutes at W3C.org. I bet there are a lot of interesting things I missed, so I encourage you to read it.

RESOLVED: Change the capture mode for all view-transitions and specify how each property is affected by this capture mode change

RESOLVED: Describe categorization of properties in the Module Interactions sections of each spec

RESOLVED: Blink will experiment and come back with changes needed if there are compat concerns