Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-view-transitions-2] CSS only way to transition between list <-> detail views #8209

Open
matthias-margin opened this issue Dec 9, 2022 · 27 comments
Labels
css-view-transitions-2 View Transitions; New feature requests

Comments

@matthias-margin
Copy link

Currently, the spec only adds one new CSS property for all elements view-transition-name. This results in some awkward JS required to support transitions between a list view (of video thumbnails for example) and a detail view (of a video player) that has to add the right view-transition-name to the clicked thumbnail for transition into the detail view, and find the right thumbnail based on the video view.

The list <-> detail views are a common enough use case that I think it warrants a way to address this in the API directly. One way this could be done in just CSS today would be to define a different view-transition-name for each element in the list and then use the same one for its detail view.

// index.html
<img class="video-thumbnail" style="view-transition-name: video-1" />
<img class="video-thumbnail" style="view-transition-name: video-2" />
<img class="video-thumbnail" style="view-transition-name: video-3" />

// detail-2.html
<video class="video-player" style="view-transition-name: video-2" />

This works fine if you can rely on the default transition, but if you want anything else, then you'll have to start adding rules for each of the transition names. This is not very scalable or desirable:

::view-transition-old(video-1),
::view-transition-old(video-2),
::view-transition-old(video-3) { ... }

::view-transition-new(video-1),
::view-transition-new(video-2),
::view-transition-new(video-3) { ... }

proposal

Introduce a new view-transition-id that can be paired with view-transition-name to target and generate animations. CSS could then be written to target combination of name, name/id or name/any id. Eg:

// index.html
<img class="video-thumbnail" style="view-transition-id: 1" />
<img class="video-thumbnail" style="view-transition-id: 2" />
<img class="video-thumbnail" style="view-transition-id: 3" />

// detail-2.html
<video class="video-player" style="view-transition-id: 2" />

// css 
.video-thumbnail {
  view-transition-name: video
}
.video-player{
  view-transition-name: video
}

// define transition for all video views
::view-transition-old(video) { ... }
::view-transition-new(video) { ... }

// define transition for the video view that has a matching new view
::view-transition-old(video matched) { ... }
::view-transition-new(video matched) { ... }

// define transition for video with specific id
::view-transition-old(video #1) { ... }
::view-transition-new(video #1) { ... }

The UA would then have enough information to know how to animate the specific thumbnail into the video without needing any custom JS.

Obviously the exact syntax can be different than what I propose here. The important part is to have a way to match a "class" of elements, but still be able to differentiate them during the animation based on a unique id.

Thoughts?

@tabatkins tabatkins added the css-view-transitions-1 View Transitions; Bugs only label Dec 9, 2022
@khushalsagar khushalsagar changed the title [css-shared-element-transitions-1] CSS only way to transition between list <-> detail views [css-view-transitions-2] CSS only way to transition between list <-> detail views Jan 5, 2023
@khushalsagar khushalsagar added css-view-transitions-2 View Transitions; New feature requests and removed css-view-transitions-1 View Transitions; Bugs only labels Jan 5, 2023
@khushalsagar
Copy link
Member

Just to clarify, the CSS property view-transition-name is actually more like view-transition-id that you describe above since it must be unique for each element participating in the transition. It also can be used to target a unique generated pseudo-element.

I'm supportive of having a class name like identifier which can be common to set of elements for the purpose of specifying styles which apply to a subset of pseudo-elements of a type but not all of them. So you'd have:

  • view-transition-old(*): all pseudo-elements of this type.
  • view-transition-old(foo): pseudo-element generated for the DOM element with view-transition-name: foo.
  • view-transition-old(.foo): pseudo-elements generated for DOM elements with view-transition-class: foo.

@matthias-margin
Copy link
Author

matthias-margin commented Jan 6, 2023

Could you apply both a class and a name at the same time? If so this may be enough. Given this transition:

From HTML:

<img style="view-transition-class: video; view-transition-name: 123abc" />
<img style="view-transition-class: video; view-transition-name: 456def" />

To HTML:

<video class="video-player" style="view-transition-class: video; view-transition-name: 123abc" />

The transition would generate these pseudo-elements?

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(.video 123abc)
│  └─ ::view-transition-image-pair(.video 123abc)
│     ├─ ::view-transition-old(.video 123abc)
│     └─ ::view-transition-new(.video 123abc)
└─ ::view-transition-group(.video 456def)
   └─ ::view-transition-image-pair(.video 456def)
      └─ ::view-transition-old(.video 456def)

Which could be targeted with this CSS:

::view-transition-old(.video) { /* default transition out animations */
::view-transition-old(.video):has(+ ::view-transition-new) { /* transition out for matching element */ }

Does this look right?

@jakearchibald
Copy link
Contributor

jakearchibald commented Jan 17, 2023

I think part of this problem would be solved by transitions 'classes' #8319.

However, there's another part to this. If you have 50 list items, and one detail view, this will result in 50 transition groups, all but one outgoing. That's pretty bad for performance. #8282 might allow some culling, but it still isn't ideal.

It seems like, in this case, we really want the developer to limit the transition to the single clicked thumbnail. I don't immediately see a way to solve this, as the browser won't know what's in the new state when capturing the old state.

@KevinDoughty
Copy link

A view-transition-id would also permit an implicit form of view transitions. With IDs that can be sorted by an event handler that establishes identity instead of equality, element removal could be implicitly animated by code like this:

element.parentNode.removeChild(element);

Implicit might not be the best API for a shared element list to detail transition, but would be very useful in many other cases.

@jakearchibald
Copy link
Contributor

Are you suggesting that, an element with a view-transition-id should autostart a view transition when it's removed? Why just that and not, say, on class name change, or inline style change, or on content change etc etc?

@KevinDoughty
Copy link

KevinDoughty commented Jan 18, 2023

You're right, they can't share a pseudo-element because, yes I propose that removal automatically trigger animation. In fact, I've confused pseudo-classes and pseudo-elements. Implicit element removal animation would need two pseudo-classes, like view-animation-out and view-animation-in, for selection, and a children event handler to sort. A default sort could look for a data attribute like data-animation-id.

My hope was to piggyback onto your API and animate out using those pseudo-elements of yours. But I understand now that won't work, if it triggered your API or allowed a redundant way of styling. My experiments with discrete-state suggest that it might be possible to use live elements, and have the browser report discrete destination values only.

Sorry for being sloppy, I'll be a lot more precise after implementing it.

@jakearchibald
Copy link
Contributor

I don't know if it's what you're referring to, but you can create exit and entry animations with view transitions: https://developer.chrome.com/docs/web-platform/view-transitions/#custom-entry-and-exit-transitions

@calinoracation
Copy link

We are running into the same for our list detail views. JavaScript has been our solution so far but it's not ideal. One thought I was pursuing was since it's generally a link or button triggering the transition, that I could use :has and :focus-within to add the view-transition-name to the item being opened. The other challenge with that though is the navigation back. I had to track which item was opened and ensure it was set, so that again required js. In a future world and spa a style container query and variable set before might help, but alas that's complex. Glad other solutions here are being explored..

@khushalsagar
Copy link
Member

@jakearchibald had an idea which came up in the context of MPA API exploration that could be relevant here. Having a global name for the Document (or DOM state in the SPA case) that can be used as a CSS media query to conditionally name things. Super rough sketch:

// When navigating from index to details page.
document.startViewTransition(updateCallback, {old-state: "index", new-state: "item1"});

// When navigating back from details page to index.
document.startViewTransition(updateCallback, {old-state: "item1", new-state: "index"});

Which can then be used in CSS on the index page to apply names:

/* Only name this item if we're navigating to/from this item's details page */
@media (view-transition-new-state: item1) {
   view-transition-name: item;
}
@media (view-transition-old-state: item1) {
   view-transition-name: item;
}

I'm assuming setting the old-state/new-state when calling startViewTransition is feasible since updateCallback is bound to know where the navigation is going.

@calinoracation
Copy link

calinoracation commented Mar 9, 2023

That's super interesting and love the direction. Thinking about it from our perspective and we wrap startViewTransition currently but perhaps we'd be able to look at the event trigger in a handler to know what triggered it, ie: item1. We generally let anything such as a button click or even history navigation trigger the startViewTransition, so really trying to ponder how exactly we'd know the old-state/new-state in that context.

Thought about this and am trying it out and I do think we have access to both old & new state at the time of calling start. Think this is a really solid pattern so far.

Still am very curious how this might work eventually in a CSS triggered activation.

@noamr
Copy link
Collaborator

noamr commented Jun 5, 2023

I wonder if instead of addressing this as a view-transitions problem, we should look at this from the point of view of a elements.
The same way we have the :active and :target pseudo-classes, we could have :next and :previous (we can bikeshed the names, can be :outgoing and :incoming). When a link is clicked, it would be the :outgoing or :next link. Links that would lead to the previously displayed URL would be :incoming or :previous.

So given a list of mini-posters that goes to a maxi-poster:

a.miniposter:is(:next, :previous) {
  view-transition-name: poster;
} 

h1.maxiposter {
  view-transition-name: poster;
}

As a side-benefit, this would also let us curate cross-document view-transitions based on incoming URLs:

html:has(a#home:previous) {
 // customize transition based on the previous URL being "home"
}

@khushalsagar
Copy link
Member

I'm assuming the next and previous pseudo-classes in the suggestion above would activate only on the element with the href that initiated the navigation?

That would only work for a narrow set of cases. For example, you have a link underneath an image for the list entry. How do you tag that image element in this case?

@noamr
Copy link
Collaborator

noamr commented Jun 5, 2023

I'm assuming the next and previous pseudo-classes in the suggestion above would activate only on the element with the href that initiated the navigation?

Yes. We can also call it :last-clicked or so

That would only work for a narrow set of cases. For example, you have a link underneath an image for the list entry. How do you tag that image element in this case?

.item:has(a:next) img.poster {
}

// or
img:has(> a:last-clicked) {
}

@khushalsagar
Copy link
Member

Ah, so with .item:has(a:next) img.poster, the first part checks if any DOM element in the subtree for a list item activates that pseudo-class. And if so, you can apply styles to any other element in that sub-tree?

@noamr
Copy link
Collaborator

noamr commented Jun 5, 2023

Ah, so with .item:has(a:next) img.poster, the first part checks if any DOM element in the subtree for a list item activates that pseudo-class. And if so, you can apply styles to any other element in that sub-tree?

Yep!
CSS is very expressive when it's used with the DOM.
So expressing something like next/previous navigation in the DOM itself rather than in some media-query gives a lot of flexibility.

@khushalsagar
Copy link
Member

Ok, I got it now. I'm still unsure of how we're solving the back navigation. Say the user goes from list page to details page for a list item. Now they can either go back (by clicking the back button in the browser) or go to the home page (by clicking a link on the details page). You only want to tag the hero image on the details page if the user is going back to the list page. Going back to home page doesn't need the hero image to do an independent animation.

How would the author set this up?

@bramus
Copy link
Contributor

bramus commented Jun 5, 2023

Have been pondering about this for quite some time, but never got round to replying here.

An alternative approach I was thinking of was to allow wildcards at the end of names and pseudos.

.maxiposter {
  view-transition-name: poster-*; /* Any view-transition-name starts with poster- may transition into me */
}

#miniposter-123 {
  view-transition-name: poster-123; /* Can transition into .maxiposter as the vt-name has the correct prefix */
}
#miniposter-abc {
  view-transition-name: poster-abc; /* Can transition into .maxiposter as the vt-name has the correct prefix */
}

::view-transition-group(poster-*) {
  /* … */
}

As for back navigations, I would expect the browser to run the animations that ran in reverse, as it knows what was clicked, what the scroll position was, etc.

@noamr
Copy link
Collaborator

noamr commented Jun 5, 2023

Ok, I got it now. I'm still unsure of how we're solving the back navigation. Say the user goes from list page to details page for a list item. Now they can either go back (by clicking the back button in the browser) or go to the home page (by clicking a link on the details page). You only want to tag the hero image on the details page if the user is going back to the list page. Going back to home page doesn't need the hero image to do an independent animation.

How would the author set this up?

First of all, this is a somewhat different use case, it's not related exactly to list<->details.
But anyway:

body:has(link.details:next) #maxiposter {
  view-transition: poster;
}

We have to figure out some details though, because in the case of "Back" several links could be "next" and also we have to have UA-back in mind.

@noamr
Copy link
Collaborator

noamr commented Jun 5, 2023

Have been pondering about this for quite some time, but never got round to replying here.

An alternative approach I was thinking of was to allow wildcards at the end of names and pseudos.

.maxiposter {
  view-transition-name: poster-*; /* Any view-transition-name starts with poster- may transition into me */
}

#miniposter-123 {
  view-transition-name: poster-123; /* Can transition into .maxiposter as the vt-name has the correct prefix */
}
#miniposter-abc {
  view-transition-name: poster-abc; /* Can transition into .maxiposter as the vt-name has the correct prefix */
}

::view-transition-group(poster-*) {
  /* … */
}

As for back navigations, I would expect the browser to run the animations that ran in reverse, as it knows what was clicked, what the scroll position was, etc.

This doesn't solve the issue, you still have to capture many snapshots when clicking a link in the list page.

@khushalsagar
Copy link
Member

First of all, this is a somewhat different use case, it's not related exactly to list<->details.

Agreed, going to a page other than the list page is a different use-case. I mentioned it because if we have a proposal which can solve this and the list use-case, we'd favour that over a proposal which only helps with the list use-case.

In the proposal below, the current Document needs to have link to the new Document in their html. It also looks like the presence of that link doesn't help narrow down which element to target. body:has(link.details:next) is equivalent to saying "if the user is navigating to this link". The browser back button is one example, I'm not sure if there will be others where authors are forced to add link elements just to use this CSS. Maybe that's ok.

body:has(link.details:next) #maxiposter {
  view-transition: poster;
}

I'm evaluating this against the media query option:

@media (next: urlpattern(...)) {
   #maxiposter { view-transition-name: poster; }
}

It seems like both would be functionally equivalent (do correct me if I'm missing a case). The media query one seems more ergonomic to me but I'd love dev feedback on that.

@noamr
Copy link
Collaborator

noamr commented Jun 6, 2023

First of all, this is a somewhat different use case, it's not related exactly to list<->details.

Agreed, going to a page other than the list page is a different use-case. I mentioned it because if we have a proposal which can solve this and the list use-case, we'd favour that over a proposal which only helps with the list use-case.

In the proposal below, the current Document needs to have link to the new Document in their html. It also looks like the presence of that link doesn't help narrow down which element to target. body:has(link.details:next) is equivalent to saying "if the user is navigating to this link". The browser back button is one example, I'm not sure if there will be others where authors are forced to add link elements just to use this CSS. Maybe that's ok.

body:has(link.details:next) #maxiposter {
  view-transition: poster;
}

I'm evaluating this against the media query option:

@media (next: urlpattern(...)) {
   #maxiposter { view-transition-name: poster; }
}

This is great, though I would probably have something like

@navigation details {
   target: urlpattern(...);
   auto-view-transition: same-origin;
   type: navigate | back-forward;
}

It seems like both would be functionally equivalent (do correct me if I'm missing a case). The media query one seems more ergonomic to me but I'd love dev feedback on that.

However, your suggestion and the rule alternative would require a media-query per item in the list, unless there's some way to use extracted URL-pattern parameters in selectors, e.g.:

@media (next: urlpattern(/details/:id)) {
   .miniposter#{:id} {
     view-transition: poster;
   } 
}

Perhaps we need both?

@navigation list-details {
    target: urlpattern(/list), urlpattern(/details?id=:id);
}

@media (navigation: list-details) {
   #maxiposter {
     view-transition-name: poster;
   }

   .item:has(:next, :prev) .poster {
     view-transition-name: poster;
   }
}

@khushalsagar
Copy link
Member

Had an offline chat with @noamr today. I realized I hadn't completely followed the problem of "a media-query per item in the list". If we only have the media query, then the author is forced to generate a rule for each item in the list. The problem gets worse with a list using infinite scroll.

So I do like the idea of a pseudo-class which activates on a href if its link matches the outgoing URL. Noam had a nice suggestion of the pseudo-class building on top of the media query with something like:

@navigation list-details {
    target: urlpattern(/list), urlpattern(/details?id=:id);
}

a:navigating-to(list-details) .poster {
   view-transition-name: poster;
}

@bramus
Copy link
Contributor

bramus commented Aug 25, 2023

I’m thinking of a new pseudo to indicate “hey, this was the element that initiated the navigation” to solve this problem.

I’m using :last-active here, but the name is definitely up for bikeshedding:

a:last-active {
  /* This applies to the link that activated a navigation */
}

Using :last-active, authors can conditionally declare view-transition-name values. In the example below only the .card – one of many – that contains the link that was actually clicked will set view-transition-name values on its elements.

.overview .card:has(a:last-active) {
  h2 {
    view-transition-name: title;
  }
  img.hero {
    view-transition-name: hero;
  }
}

#detail {
  h1 {
    view-transition-name: title;
  }
  header img {
    view-transition-name: hero;
  }
}

(This new pseudo is needed because the regular :active cannot be used, as the clicked element becomes inactive again upon navigation)

@calinoracation
Copy link

Personally would love if whatever route was chosen didn't require urlpattern, I feel like for a lot of use-cases those are driven from the server so it's hard to be sure of any specific pattern.

Really like the option of the pseudo. I'm not sure it's a big deal but if someone handled a click event on a button (ie: form submission or action + redirect), then I'm guessing we'd not have a way to indicate that with the current proposed options. Again not huge as we typically favor anchors, but thought it'd be worth mentioning.

@calinoracation
Copy link

With :last-active, would there by any issues with not having an explicit back/forward option?

For example if going from Home > Search > Details and then you land back on Search. In this case you might use forward to go back to Details in which case :last-active works, but if you go back to Home, would the :last-active be from the a clicked going from Home -> Search or from Details back to Search?

@bramus
Copy link
Contributor

bramus commented Aug 28, 2023

For example if going from Home > Search > Details and then you land back on Search. In this case you might use forward to go back to Details in which case :last-active works, but if you go back to Home, would the :last-active be from the a clicked going from Home -> Search or from Details back to Search?

I don’t think I fully understand the page flow. The idea for :last-active is that it would only apply on the clicked link (or the pressed button). So if you follow a link back to home it would apply to that link, but if you use the browser’s back button then it doesn’t apply to anything.

@calinoracation
Copy link

calinoracation commented Sep 7, 2023

So if you follow a link back to home it would apply to that link, but if you use the browser’s back button then it doesn’t apply to anything.

That makes sense. I guess I was hoping any solution would solve for user's back button or swipe navigation gestures as well. The :navigating-to pseudo seems like it might do that pretty solidly though. Seems like the :last-active is solving for selecting the item in the list to make as the target from a group. Perhaps 2 parts of the same issue and both are needed, just the :navigating-to or similar for the "new" page and :last-active for the "old" page.

Would their be an equivalent :navigating-from pseudo as well to solve the case of going from "List of 1, 2, 3", click on "2", then back to "List 1, ,2, 3" and wanting to shrink the primary image on "2" back into the list when navigating back?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-view-transitions-2 View Transitions; New feature requests
Projects
None yet
Development

No branches or pull requests

8 participants