View Transition List Reordering (with a Kick Flip)
View Transition List Reordering (with a Kick Flip) êŽë š
I remember when we first got animations and transitions in CSS on the web (ok grandpa), the talk around it was balanced between oooo! fun! shiny! and actually, movement is more than aesthetics; it can help people understand what is happening in user interfaces.
The example that got stuck in my head was reordering lists. Imagine a single list item being plucked off and moved to the top. If that instantly happens, it can be hard to catch what even happened. But if you animate the movement, it can be extremely obvious what is happening.
Works, but it is not particularly easy to understand what is happening:
More fun and easier to understand what is happening:
The List
Weâre talking a regular ol list. Perhaps ironic that weâre ordering and unordered lists, but Iâll leave that as a semantic thoughtworm for the reader.
Each list item has text, then a button which the intended action is that, when clicked, will move the list item to the top.
<ul class="list">
<li>
Apples
<button hidden disabled aria-label="Move to Top">
<svg ...></svg>
</button>
</li>
<li>
Oranges
<button hidden disabled aria-label="Move to Top">
<svg ...></svg>
</button>
</li>
<li>
<button hidden disabled aria-label="Move to Top">
<svg ...></svg>
</button>
Mangos
</li>
<li>
<button hidden disabled aria-label="Move to Top">
<svg ...></svg>
</button>
Bananas
</li>
</ul>
Note that each button has a text label (as weâre not using text inside the button), and a hidden
attribute weâll use to make sure the button isnât there at all when JavaScript is disabled.
Scaffolding the Interactive JavaScript
This will get us references to the elements we need, as well as do a loop and un-hide the buttons as well as attach an event listener to them:
const button = document.querySelector("button");
const list = document.querySelector(".list");
let listItems = list.querySelectorAll("li");
const listItemButtons = list.querySelectorAll("li > button");
listItemButtons.forEach((button) => {
button.hidden = false;
button.addEventListener("click", async () => {
// do stuff
});
});
Moving the List Item to the Top
When the button is clicked, weâll need the list item, not the button itself, so we reach up a level to the parent. Then we freshly figure out what the first list item is, and insertBefore
it, making the clicked one the first one.
const button = document.querySelector("button");
const list = document.querySelector(".list");
let listItems = list.querySelectorAll("li");
const listItemButtons = list.querySelectorAll("li > button");
listItemButtons.forEach((button) => {
button.hidden = false;
button.addEventListener("click", async () => {
const item = button.parentElement;
const firstListItem = list.querySelector(".list :first-child");
list.insertBefore(item, firstListItem);
// This is probably the better API to use, but less supported...
// list.moveBefore(item, firstListItem);
});
});
I only recently learned about moveBefore
which is probably a better API to use, but we can wait a bit for better support.
(Progressively Enhanced) Movement via View Transitions
One type of View Transitions are âsame pageâ View Transitions, where we essentially call document.startViewTransition
and change the DOM inside the callback.
const button = document.querySelector("button");
const list = document.querySelector(".list");
let listItems = list.querySelectorAll("li");
const listItemButtons = list.querySelectorAll("li > button");
function moveListItemFirst(item) {
const firstListItem = list.querySelector(".list :first-child");
list.insertBefore(item, firstListItem);
}
listItemButtons.forEach((button) => {
button.hidden = false;
button.addEventListener("click", async () => {
const item = button.parentElement;
if (document.startViewTransition) {
const transition = document.startViewTransition(() => {
moveListItemFirst(item);
});
} else {
moveListItemFirst(item);
}
});
});
Because we need to move the list item whether the browser supports View Transitions or not, we abstract that to a function, and call it on either branch of logic testing that support.
This will immediately do a fade transition for the list items, which honestly isnât much of an improvement in this case (it still can be nice for the other type of View Transitions: page transitions). Fortunately, weâve got a pretty decent one-line fix in CSS:
ul {
li {
view-transition-name: match-element;
}
}
If youâve played with View Transitions before, itâs likely youâve got in your head that every single element needs a unique view-transition-name
. And thatâs still true in Firefox for now, as only Chrome and Safari are supporting match-element
as I write. But as weâre just playing here, this is such a nice improvement and reduces so much fiddliness, I think itâs worth it.
Special View Transitions Only For the âMain Moving Elementâ
The deal here is really that all the elements are moving. Itâs either the element you clicked on moving to the first position, or the rest of the list items moving out of the way.
So the goal here is to apply a unique view-transition-name
to the element that is the âmain moving elementâ, then remove it once itâs done. To make matters a bit more difficult, weâve got two animations we want to apply, one of the list item, and one just for the icon within the button. Thatâs slightly tricky!
const button = document.querySelector("button");
const list = document.querySelector(".list");
let listItems = list.querySelectorAll("li");
const listItemButtons = list.querySelectorAll("li > button");
function moveListItemFirst(item) {
const firstListItem = list.querySelector(".list :first-child");
list.insertBefore(item, firstListItem);
}
listItemButtons.forEach((button) => {
button.hidden = false;
button.addEventListener("click", async () => {
const item = button.parentElement;
item.style.viewTransitionName = "woosh";
item.querySelector("svg").style.viewTransitionName = "tony-hawk";
if (document.startViewTransition) {
const transition = document.startViewTransition(() => {
moveListItemFirst(item);
});
try {
await transition.finished;
} finally {
item.style.viewTransitionName = "";
item.querySelector("svg").style.viewTransitionName = "";
makeFirstListItemsButtonDisabled();
}
} else {
moveListItemFirst(item);
}
});
});
Now weâve got âwooshâ and âtony-hawkâ view transition names we can use to apply animation control in CSS.
::view-transition-group(*) {
animation-duration: 1s;
}
::view-transition-old(woosh),
::view-transition-new(woosh) {
animation: woosh 1s ease-in-out;
}
@keyframes woosh {
50% {
translate: -100px 0;
scale: 1.5;
box-shadow: 0 30px 15px lch(0% 0 0 / 50%);
}
}
::view-transition-old(tony-hawk),
::view-transition-new(tony-hawk) {
animation: tony-hawk 1s ease-in-out;
}
@keyframes tony-hawk {
/* sick kick flip */
50% {
rotate: 20deg;
scale: 2.5;
}
}
So for the ânon-mainâ elements, they just move up and down over 1s. But for the âmainâ moving element, weâve got these unique @keyframe
animations we apply while the re-ordering is happening. Note that the keyframes are only applying the 50%
keyframe, so they animate from wherever they were to wherever they are going still, just in the middle they do something special, like the sick kick flip.
Video
Iâm playing with streaming and this idea started as a loose idea for a stream, then I lightly edited it for a regular YouTube video, so maybe youâd enjoy that: