w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.42k stars 652 forks source link

[css-view-transitions-2] Syntax for navigation-matching #8925

Open noamr opened 1 year ago

noamr commented 1 year ago

Proposing a syntax for matching one or more "navigations" as being the current ones, to be matchable in media-queries (and later other places).

A lot of the issues with view-transitions, especially cross-document, are around using different transitions based on different characteristics of the navigation:

8784 (different transition for reload)

8685 (different transition for back/forward)

8683 (different transition for page type, e.g. home->article vs. articles->article)

8209 (list<->details)

8048 (opt-in for cross-document transitions)

These issues circle around some sort of url pattern matching, but adding url patterns to media queries seems verbose and might create duplications.

The proposal here is to use a @ rule that names a navigation-matcher, and then use it in media queries. For example:

@navigation to-article {
   match: urlpattern(/article);
   type: navigation back-forward;
   direction: outgoing;  
}

@media (navigation: to-article) {
   a.article { view-transition: hero };
}

In the spirit of #8677 (keeping MPA/SPA APIs compatible), the concept of navigation-matching here is not specific to MPAs.

A navigation always has an "old" and "new" URL, and a type. The current navigation is updated in the following scenarios:

Notes:

@auto-view-transitions: same-origin; // bikeshedding the opt-in, see #8048
@media (navigation: to-article) {
@auto-view-transitions: none;
}
noamr commented 1 year ago

/cc @khushalsagar @tabatkins

jakearchibald commented 1 year ago

I think I like the direction of this, but some of it isn't clear to me. Can you show some code for a basic slide-from-the-side transition between two particular URL patterns, that would work for both MPA and SPA, and both push navigations and traversals?

noamr commented 1 year ago

I think I like the direction of this, but some of it isn't clear to me. Can you show some code for a basic slide-from-the-side transition between two particular URL patterns, that would work for both MPA and SPA, and both push navigations and traversals?

Sure! Something like the following:

// These are dynamically rendered on client-side (SPA) or server-side (MPA) to match the relevant URL pattern.
// e.g. the following would be rendered when the URL matches `/slide2`:

@navigation prev-slide {
  target: urlpattern(/slide1);
  type: navigate, back-forward;  
}

@navigation next-slide {
  target: urlpattern(/slide3);
  type: navigate, back-forward;  
}
//////

::view-transition-old(root) {
    animation-name: none;
}

@media (navigation: next-slide) {
  ::view-transition-new(root) {
    animation-name: slide-in-from-right;
  }

@media (navigation: prev-slide) {
  ::view-transition-new(root) {
    animation-name: slide-in-from-right;
    animation-direction: reverse;
  }
}

The idea is that we don't need to know the "current" URL, the framework or whoever already knows this. So all the navigation rules apply to the target URL, whether it's the next or the previous one. In this scenario we only affect the pseudo-elements so we don't need to worry about outgoing transitions. If we ever want to support declarative SPA transitions we can allow separating to from/to, we can also consider doing this now.

jakearchibald commented 1 year ago

That doesn't seem quite right, as it would result in a partial sliding animation if going from next-slide to some other kind of page.

I'm still not sure how I'd use this in an SPA. How would I activate these rules in an SPA?

jakearchibald commented 1 year ago

What's the benefit in separating navigate and back-forward? When would you want to do something different when traversing forward between two pages, or a navigation between two pages?

noamr commented 1 year ago

That doesn't seem quite right, as it would result in a partial sliding animation if going from next-slide to some other kind of page.

Sure, need to play with it with a class-based polyfill, hard to whiteboard the exact thing.

I'm still not sure how I'd use this in an SPA. How would I activate these rules in an SPA?

   function Slide({index: number}) {
     return <>
       <style>{` // dangerousReactHTMLStuff or a style component or whatever
@navigation from-prev-slide {
  target: urlpattern(/slide${index - 1});
}

@navigation from-next-slide {
  target: urlpattern(/slide${index + 1});
}
`}</style>
   <main>{slideContent[index]}</main>
     </>
   }
noamr commented 1 year ago

What's the benefit in separating navigate and back-forward? When would you want to do something different when traversing forward between two pages, or a navigation between two pages?

Probably in most cases you won't, so navigation, back-forward would likely be the default, and you can change to navigation if you don't want history traversals to trigger the animation. I can imagine that in some cases the transition would signify something "new" and you wouldn't want to repeat it. What was the use case for you for opening #8685 btw?

I think one of the main use-cases for the navigation-type is actually to enable/disable animation on reload (#8784).

jakearchibald commented 1 year ago

I still don't think I'm seeing a full example here, and it's already looking complicated to do part of an extremely basic example, which suggests the design has gone wrong somewhere.

For contrast, here's how I'd do the slide-between two pages example based on the proposal in https://github.com/w3c/csswg-drafts/issues/8683:

@media (vt-next-page: urlpattern('/foo')) {
  @cross-document-transition allow;
}

@media (vt-old-page: urlpattern('/foo')) and (vt-page: urlpattern('/bar')) {
  @cross-document-transition allow;

  ::view-transition-new(root) {
    animation-name: slide-from-right;
  }

  ::view-transition-old(root) {
    animation-name: slide-to-left;
  }
}

Then, to make it work in SPA too:

document.startViewTransition({
  oldURL: '/foo',
  newURL: '/bar',
  update() {
    // update DOM here
  }
});

Where oldURL and newURL would activate the various media queries for the transition.

And that's a full example, and it didn't need frameworks, generated CSS etc etc.

jakearchibald commented 1 year ago

What was the use case for you for opening #8685 btw?

The one stated in the issue: Transitions tend to happen in the opposite direction on 'back' navigations. Note that this doesn't just mean reversing the animation.

I think one of the main use-cases for the navigation-type is actually to enable/disable animation on reload (#8784).

I think it'll be super-rare or even plain undesirable to want transitions on reload, so I'd just make that an extra opt-in, or just don't allow it to happen.

noamr commented 1 year ago

I still don't think I'm seeing a full example here, and it's already looking complicated to do part of an extremely basic example, which suggests the design has gone wrong somewhere.

For contrast, here's how I'd do the slide-between two pages example based on the proposal in #8683:

@media (vt-next-page: urlpattern('/foo')) {
  @cross-document-transition allow;
}

@media (vt-old-page: urlpattern('/foo')) and (vt-page: urlpattern('/bar')) {
  @cross-document-transition allow;

  ::view-transition-new(root) {
    animation-name: slide-from-right;
  }

  ::view-transition-old(root) {
    animation-name: slide-to-left;
  }
}

Then, to make it work in SPA too:

document.startViewTransition({
  oldURL: '/foo',
  newURL: '/bar',
  update() {
    // update DOM here
  }
});

Where oldURL and newURL would activate the various media queries for the transition.

How do you disable reloads or give them a different animation in this example? It seems great when it's simple but when you want to tweak it you might end up with monstrous media-queries. Also, this is VT-specific. By having this as "navigations", you could style other things based on where you're coming from.

Regarding having the URLs in the startVT function, sure, why not. I was thinking of something that would be declarative in advance, but putting this directly in startVT makes sense since it let's the SPA framework define what a navigation is.

@navigation to-foo{
  to: urlpattern(/foo);
  type: navigate, back-forward, reload; // reload OK!
}

@navigation foo-to-bar {
  from: urlpattern(/foo);
  to: urlpattern(/bar);
}

@media (navigation: to-foo) or (navigation: foo-to-bar) {
  @cross-document-transition allow;
}

@media (navigation: foo-to-bar) {
  ::view-transition-new(root) {
    animation-name: slide-from-right;
  }

  ::view-transition-old(root) {
    animation-name: slide-to-left;
  }
}

btw this is a push animation and not a slide animation.

noamr commented 1 year ago

The thing with putting URL pattern in the media-query, is that if your routes change you have to update all of them, and you might have many. Sprinkling navigation routes all around CSS files seems brittle. Note also that URL patterns can be quite long and bulky, with RegEx etc.

So by putting them in their own rule we can achieve several things:

The other parts of this proposal are perhaps what makes it seem complicated for the slides case:

I think I'm OK with keeping the rule but spelling-out the to/from both in the rule and in the startVT function (in the SPA case)

jakearchibald commented 1 year ago

How do you disable reloads or give them a different animation in this example?

Like I said, I'm not sure allowing transitions for reloads is a good idea. But, if we really wanted it, I'd make it part of the opt-in rule, and add a media query for it.

It seems great when it's simple but when you want to tweak it you might end up with monstrous media-queries.

Can you give an example?

Also, this is VT-specific. By having this as "navigations", you could style other things based on where you're coming from.

Maybe? But, making it work badly for VT because of some unknown non-VT use-case seems bad.

Regarding having the URLs in the startVT function, sure, why not. I was thinking of something that would be declarative in advance, but putting this directly in startVT makes sense since it let's the SPA framework define what a navigation is.

Right, this is why I was asking you for an example, because I couldn't see how 'automatic' ways of doing it would fit in with the timing of same-document navigations and traversals.

btw this is a push animation and not a slide animation.

Huh, I didn't realise there were official names for these types of animations. Where are they defined?

By 'slide' I was meaning that the new content slides in from the right (or left if it's 'back'), and the old content slides out to the left (or right if it's 'back'). But, the specific animation doesn't really matter, other than it's directional and between two specific pages.

The thing with putting URL pattern in the media-query, is that if your routes change you have to update all of them, and you might have many. Sprinkling navigation routes all around CSS files seems brittle.

Agreed. When I said "I think I like the direction of this" I was referring to creating named definitions. I think that's a good idea.

Btw, I'm not saying the existing ideas I've summarised in various issues are perfect or even good, but they were thought-through, so they're a useful basis for comparison.

I think I'm OK with keeping the rule but spelling-out the to/from both in the rule and in the startVT function (in the SPA case)

Yeah, I think exploring this further is a good idea. Just test it against basic examples, and ensure that it works for SPA too, so developers don't end up with multiple definitions for the same animation between two 'pages'.

If options are added to startViewTransition, I urge you to avoid the functionName(bigCallback, options) pattern. I think there's a path forward here, but it's novel https://github.com/whatwg/webidl/issues/1191

noamr commented 1 year ago

@jakearchibald btw with the URLs in the SPA case, perhaps we don't need new parameters?

// old URL is the current document URL when the old state is captured
document.startViewTransition(() => {
   // new URL is the document URL when the new state is captured.
   history.push(newURL);
});
jakearchibald commented 1 year ago

I don't see how that would fit in with the timing of same-document traversals. Being able to control when the URL is updated for any navigation is still experimental in the navigation API https://github.com/WICG/navigation-api/issues/66#issuecomment-1527047126

noamr commented 1 year ago

I don't see how that would fit in with the timing of same-document traversals. Being able to control when the URL is updated for any navigation is still experimental in the navigation API WICG/navigation-api#66 (comment)

Oh because the document's URL is the new one on popstate. But perhaps we could have something as reasonable defaults, e.g. if new URL defaults to document URL at time of capture-new and old URL defaults to URL at capture-old, we could do with passing only oldURL on popstate and have the rest "just work".

jakearchibald commented 1 year ago

Yeah, that might be nice and forward-looking.

document.startViewTransition({
  urls,
  update() {
    // update DOM here
  }
});

Where urls can be:

Maybe that's a bit of a weird API shape, since {} would be equivalent to "auto". But I like the idea.

noamr commented 1 year ago

Yeah, that might be nice and forward-looking.

document.startViewTransition({
  urls,
  update() {
    // update DOM here
  }
});

Where urls can be:

  • undefined - default, don't do anything with the media queries
  • "auto" - pick up the URLs automatically
  • { old, new } - the URLs. If either are left undefined, then they're picked up automatically.

Maybe that's a bit of a weird API shape, since {} would be equivalent to "auto". But I like the idea.

In general I like the idea of callback | {callback, ...}. I'm not sure we need undefined though. If you don't want to deal with URLs, don't define @navigation rules / media-queries.

jakearchibald commented 1 year ago

I'm not sure we need undefined though. If you don't want to deal with URLs, don't define @navigation rules / media-queries.

Hmm, not all view transitions are "page" transitions. That's why I don't think the media queries should apply by default.

khushalsagar commented 1 year ago

@jakearchibald before going into the exact syntax, I want to make sure we're all on the same page about the following:

@media (vt-next-page: article) {
  /* … */
}

where article comes from the new Document makes the implementation quite complicated. It's more preferable to have a syntax where the old Document uses the target URL of the navigation to avoid this dependency.

noamr commented 1 year ago

@jakearchibald before going into the exact syntax, I want to make sure we're all on the same page about the following:

  • Not allowing media queries on the old Document to depend on any state parsed from the new Document. So something like this on the old Document:

Right, it should use the initiated navigation's URL in the old document, and the actual URL in the new document. Authors should be aware of this.

  • Extending this approach to SPA transitions:

    • It's better if we can enable these queries for same-document navigations, irrespective of whether there is a transition. We'll need to think through the lifetime of the queries with respect to the navigation lifecycle but I don't see the advantage of limiting it to navigations which have a ViewTransition (unless constrained by implementation).
    • On the flip side, if the transition is not associated with a navigation, then does it make sense to enable these queries? They would be tightly coupled with navigation concepts. We could have the author specify parameters that identify a navigation (url, type) in the call to startViewTransition but then it becomes ambiguous which state should we be using for these queries. For example, does the vt-old-page value come from the previous navigation entry's url or what the author passed in the startViewTransition call? We can have a think about this more but are there use-cases where an author would want this functionality but doesn't want to trigger a same-document navigation?

I am thinking that these concepts would be automatic for MPA navigations, and at first manual for SPA navigations, with an option to enable automatic navigation detection later when we allow declarative SPA transitions (something I'm really keen to do in the future). The latter would require careful integration with the timings of the navigation API,

Perhaps for SPA transitions we don't even need to pass the URLs, but rather just the navigation names, e.g.

startViewTransition({update: () => {...}, navigations: ["foo-to-bar"]})
jakearchibald commented 1 year ago

@khushalsagar

  • Not allowing media queries on the old Document to depend on any state parsed from the new Document.

Yeah, I understand the technical limitations around this.

  • It's better if we can enable these queries for same-document navigations, irrespective of whether there is a transition. We'll need to think through the lifetime of the queries with respect to the navigation lifecycle but I don't see the advantage of limiting it to navigations which have a ViewTransition (unless constrained by implementation).

There are pros and cons to that. The pros are it becomes a general navigation feature, which feels nice in terms of platform structure (although view transitions would still have an impact on the lifetime of these rules), but some cons:

@noamr

with an option to enable automatic navigation detection later when we allow declarative SPA transitions (something I'm really keen to do in the future)

Given the issues around the URL change, I don't think declarative SPA transitions offer a lot of value. The developer would need to use the opt-in, switch their routing to the navigation API, enable manual entry committing, and ensure they commit the entry at the correct point. This seems like orders of magnitude more work than just using document.startViewTransition.

Perhaps for SPA transitions we don't even need to pass the URLs, but rather just the navigation names, e.g.

startViewTransition({update: () => {...}, navigations: ["foo-to-bar"]})

I don't think these navigation definitions should include both the old and new URL patterns. I see the benefit of defining a bunch of paths as a particular page type, but forcing the rule to include both patterns just reintroduces the repetition in a different place.

For example, if you have 4 page types, "index", "article", "gallery", "search-results", and some customisation of transitions between them, you now have 16 definitions:

  1. index-to-index
  2. index-to-article
  3. index-to-gallery
  4. index-to-search-results
  5. article-to-index
  6. article-to-article
  7. article-to-gallery
  8. article-to-search-results
  9. gallery-to-index
  10. gallery-to-article
  11. gallery-to-gallery
  12. gallery-to-search-results
  13. search-results-to-index
  14. search-results-to-article
  15. search-results-to-gallery
  16. search-results-to-search-results

Unless I'm missing something, you'd end up declaring the same URL patterns 8 times each.

Instead, it seems better if there's a way to define a page type from a bunch of URL patterns, then they're only defined once each.

noamr commented 1 year ago

@khushalsagar

  • Not allowing media queries on the old Document to depend on any state parsed from the new Document.

Yeah, I understand the technical limitations around this.

  • It's better if we can enable these queries for same-document navigations, irrespective of whether there is a transition. We'll need to think through the lifetime of the queries with respect to the navigation lifecycle but I don't see the advantage of limiting it to navigations which have a ViewTransition (unless constrained by implementation).

There are pros and cons to that. The pros are it becomes a general navigation feature, which feels nice in terms of platform structure (although view transitions would still have an impact on the lifetime of these rules), but some cons:

  • Could there be cases where a cross-document transition should be reused in an SPA, but in the SPA it doesn't really make sense as a navigation? In this case we'd be forcing the developer to create a dummy navigation just to activate these rules.

You can still use the regular view transitions and apply classes or what not, the media queries are specifically for the cases where you want the transitions to automatically react to a navigation.

  • Being able to control when the URL changes in an SPA navigation is currently an experimental feature in the navigation API. It isn't possible with the history API as the process is synchronous. Since this is limited to the navigation API, uptake in routing libraries might be slow.
  • Could this result in developers changing the URL at a sub-optimal point because it's what the view transition needs?

@noamr

I think we should advance declarative same-document transitions once the navigation API gets into shape. Yes, it has a few issues but I think it would be amazing if "some day" we could have same-document experiences that don't require loads of JS.

with an option to enable automatic navigation detection later when we allow declarative SPA transitions (something I'm really keen to do in the future)

Given the issues around the URL change, I don't think declarative SPA transitions offer a lot of value. The developer would need to use the opt-in, switch their routing to the navigation API, enable manual entry committing, and ensure they commit the entry at the correct point. This seems like orders of magnitude more work than just using document.startViewTransition.

Yup, right now it is. I hope that on top of some of the primitives of the navigation API we could make some of this stuff declarative in the future, where transitions between routes/hashes are declarative. But I wouldn't rush to it.

Perhaps for SPA transitions we don't even need to pass the URLs, but rather just the navigation names, e.g.

startViewTransition({update: () => {...}, navigations: ["foo-to-bar"]})

I don't think these navigation definitions should include both the old and new URL patterns. I see the benefit of defining a bunch of paths as a particular page type, but forcing the rule to include both patterns just reintroduces the repetition in a different place.

For example, if you have 4 page types, "index", "article", "gallery", "search-results", and some customisation of transitions between them, you now have 16 definitions:

  1. index-to-index
  2. index-to-article
  3. index-to-gallery
  4. index-to-search-results
  5. article-to-index
  6. article-to-article
  7. article-to-gallery
  8. article-to-search-results
  9. gallery-to-index
  10. gallery-to-article
  11. gallery-to-gallery
  12. gallery-to-search-results
  13. search-results-to-index
  14. search-results-to-article
  15. search-results-to-gallery
  16. search-results-to-search-results

Unless I'm missing something, you'd end up declaring the same URL patterns 8 times each.

Probably 8:

@media (navigation: from-search-results) and (navigation: to-gallery) {
}

The alternative is perhaps nicer:

@media (navigation: from search-results to gallery) {
}

I was proposing the former because it is perhaps more extendable in terms of defining things about the navigation itself rather than the page type (e.g. opt-in for back-forward, or special-casing cross-document). We should examine if those customizations are common enough to be part of the definition, or if putting them directly in the media-query is sufficient.

jakearchibald commented 1 year ago
@media (navigation: from-search-results) and (navigation: to-gallery) {
}

This pattern seems good. I was referring to other posts where you were suggesting the definition included the from and to URLs in one go.

jakearchibald commented 1 year ago

But, even better:

@media (navigation-from: search-results) and (navigation-to: gallery) {
}

Now you only need to define "search-results" URLs once, rather than twice.

Given the argument for this pattern was because the repetition of URLs is brittle, it seems right to cut out the repetition completely.

noamr commented 1 year ago

But, even better:

@media (navigation-from: search-results) and (navigation-to: gallery) {
}

Now you only need to define "search-results" URLs once, rather than twice.

I still prefer (navigation: from search-results) and also allow (navigation: search-results) if it's bidirectional. By having the direction optional/omitted by default, we make it easier to generate the simple case:

noamr commented 1 year ago

One important benefit of solving this early is that it gives us "events" out of the box, by registering to media query change events.

jakearchibald commented 1 year ago

I still prefer (navigation: from search-results)

Ok, but please talk to the CSSWG and CSS-knowledgeable folks about it.

For width, media queries use (min-width: value), not (width: min value), so make sure you have a good justification for breaking existing patterns.

noamr commented 1 year ago

I still prefer (navigation: from search-results)

Ok, but please talk to the CSSWG and CSS-knowledgeable folks about it.

For width, media queries use (min-width: value), not (width: min value), so make sure you have a good justification for breaking existing patterns.

Naturally we'll discuss it at the WG. Given 10px <= width <= 100px. I don't think we HAVE to limit ourselves to (key: value).

But anyway, I think the syntax conversation is more of a detail, and the bigger discussion is the relationship between a view-transition and a navigation. I feel that we should have a strategy around it, even if we don't implement everything in the first go.

In #8677 we defined that one of the goals is that SPA & MPA don't diverge more than necessarry. In the MPA case, every transition is a navigation, so does that mean that (document-scoped) SPA transitions should also be navigation-oriented? In a way, because document-scoped view transitions block rendering for the entire page, they feel "navigation-y" even if they don't change the URL.

The problem with treating every transition as a "navigation" is that soft navigations are not a well defined term and don't have a specified lifecycle but rather a concept that described multiple JS operations (see #8300), and the navigation API which attempts to define these terms is still experimental.

I see several directions we could go with this:

  1. leave this to userland JS, see what patterns emerge. Experiment with polyfills for this with e.g. the navigation API or something like this. Note that almost everyone using view-transitions for navigations would have to use a polyfill similar to this, or count on the routing framework (Next etc) to include that functionality.

  2. Give names to transitions/navigations, allow to declaratively link them with navigations in the MPA case, and specify them explicitly in the SPA case (some version of this. Think about making this declarative for SPA when we get to #8300.

  3. Specify an initial version #8300 now. In a way, an app that supports both SPA and MPA does have a pattern: capturing link clicks and popstate in the SPA case and not capturing them in MPA. But this might be a bit leaky until we resolve some things around the navigation API.

  4. Integrate those media queries with the navigation API. As in, only when the "well lit path" of navigating via the navigation API is followed, we trigger the navigation matchers.

Doing (1) is the easiest but might lead to footguns/antipatterns, and potentially a lot of over-capturing. It's perhaps a risk we can take and mitigate it with a lot of documentation/education, and making sure frameworks do the right thing.

OTOH, I think that (2) could be feasible with the right naming choices. A big plus for (2) is that it gives us the JS events "for free" by using MediaQuery change events.

I'm currently into researching (4).

Would love to have more opinions here... @tabatkins, @khushalsagar?

bramus commented 1 year ago

Picking out only a detail from earlier in this thread:

@navigation prev-slide {
  target: urlpattern(/slide1);
  type: navigate, back-forward;  
}

@navigation next-slide {
  target: urlpattern(/slide3);
  type: navigate, back-forward;  
}

As an author, that would mean I need to write extra CSS for each and every page that my website holds. A snippet for page 1, page 2, page 3, …, page N. While this is easily possible to generate when using an SPA or some server side scripting language, not all sites have a build step and can just be static HTML.

Ideally, to me, there’d be some way to flag the directionality from within the markup as well, or at least have a way to provide a clue from the markup to the CSS. One of the earlier ideas floated (in some other thread that I can’t find back) was to expose the navigation initiator somehow. For example, if the CSS somehow knew the users clicked a#previous-page to start the navigation, it could use that info to determine that it’s a backwards type of animation that needs to be done.

noamr commented 1 year ago

Picking out only a detail from earlier in this thread:

@navigation prev-slide {
  target: urlpattern(/slide1);
  type: navigate, back-forward;  
}

@navigation next-slide {
  target: urlpattern(/slide3);
  type: navigate, back-forward;  
}

As an author, that would mean I need to write extra CSS for each and every page that my website holds. A snippet for page 1, page 2, page 3, …, page N. While this is easily possible to generate when using an SPA or some server side scripting language, not all sites have a build step and can just be static HTML.

Ideally, to me, there’d be some way to flag the directionality from within the markup as well, or at least have a way to provide a clue from the markup to the CSS. One of the earlier ideas floated (in some other thread that I can’t find back) was to expose the navigation initiator somehow. For example, if the CSS somehow knew the users clicked a#previous-page to start the navigation, it could use that info to determine that it’s a backwards type of animation that needs to be done.

I wouldn't use the link for this, perhaps you have both a "back" and a "jump to slide 2"? If we really wanted to support this use case in the web platform I would do something like define an order between the pages when declaring them and use that as an extra semantic in the media-queries. But I think this might be too niche, specifically for slideshow-style things, and perhaps it's easy enough to generate this from SCSS/some server framework?