This tutorial will guide you through the process of how to Add/remove Cards With View Transition In Vanilla Js. Leveraging the View Transition API, you can create smooth and engaging animations when adding or removing elements from your web page, enhancing the user experience with visual continuity. This approach is particularly useful for dynamic content updates, providing a polished and modern feel to your web applications.
Setting Up the HTML Structure
First, we need to structure our HTML to include a button for adding new cards, a template for the card elements, and a container to hold the cards. Additionally, we’ll include a warning message for browsers that don’t support view transitions.
<button class="add-btn">
<span class="sr-only">Add</span>
</button>
<template id="card">
<li class="card">
<button class="delete-btn">
<span class="sr-only">Delete</span>
</button>
</li>
</template>
<ul class="cards">
<li class="card" style="view-transition-name: card-1; background-color: tan;">
<button class="delete-btn">
<span class="sr-only">Delete</span>
</button>
</li>
<li class="card" style="view-transition-name: card-2; background-color: khaki;">
<button class="delete-btn">
<span class="sr-only">Delete</span>
</button>
</li>
<li class="card" style="view-transition-name: card-3; background-color: thistle;">
<button class="delete-btn">
<span class="sr-only">Delete</span>
</button>
</li>
<li class="card" style="view-transition-name: card-4; background-color: wheat;">
<button class="delete-btn">
<span class="sr-only">Delete</span>
</button>
</li>
</ul>
<div class="warning">
<p>Your browser does not support <code>view-transtion-class: <custom-ident>+</code>. As a result, the existing cards will not bounce upon inserting/deleting a card.</p>
</div>
<footer>
<p>Icons from <a href="https://www.iconfinder.com/iconsets/ionicons-outline-vol-1">Ionicons Outline Vol.1</a>, licensed under the <a href="https://opensource.org/license/MIT">MIT license</a>.</p>
</footer>
<script src="./script.js"></script>
Styling with CSS
Next, we will style the HTML elements created in the previous step. CSS will enable view transitions and the bounce effect, we will also create the design for cards and their respective buttons.
@layer view-transitions {
/* Don’t capture the root, allowing pointer interaction while cards are animating */
@layer no-root {
:root {
view-transition-name: none;
}
::view-transition {
pointer-events: none;
}
}
/* Cards, in general, should use a bounce effect when moving to their new position */
@layer reorder-cards {
@supports (view-transition-class: card) {
.warning {
display: none;
}
:root {
--bounce-easing: linear(
0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
0.973, 1, 0.988, 0.984, 0.988, 1
);
}
.card {
view-transition-class: card;
}
/* Without view-transition-class you had to write a selector that targets all cards … and that selector needed updating whenever you added/removed a card */
::view-transition-group(*.card) {
animation-timing-function: var(--bounce-easing);
animation-duration: 0.5s;
}
}
}
/* Newly added cards should animate-in */
@layer add-card {
@keyframes animate-in {
0% {
opacity: 0;
translate: 0 -200px;
}
100% {
opacity: 1;
translate: 0 0;
}
}
::view-transition-new(targeted-card):only-child {
animation: animate-in ease-in 0.25s forwards;
}
}
/* Cards that get removed should animate-out */
@layer remove-card {
@keyframes animate-out {
0% {
opacity: 1;
translate: 0 0;
}
100% {
opacity: 0;
translate: 0 -200px;
}
}
::view-transition-old(targeted-card):only-child {
animation: animate-out ease-out 0.5s forwards;
}
}
}
/* Etc. */
@layer base {
* {
box-sizing: border-box;
}
body {
display: grid;
height: 90dvh;
place-items: center;
padding: 2rem 0;
font-family: system-ui, sans-serif;
width: 100%;
}
.cards {
padding: 0;
display: flex;
justify-content: center;
width: 100%;
gap: 2rem;
padding: 1rem 2rem;
overflow-y: auto;
overscroll-behavior: contain;
/* flex-wrap: wrap; */
}
.card {
width: 100%;
aspect-ratio: 2/3;
display: block;
position: relative;
border-radius: 1rem;
max-width: 10vw;
min-width: 50px;
background-color: grey;
}
.delete-btn {
--icon: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI1MTIiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZS8+PHBhdGggZD0iTTExMiwxMTJsMjAsMzIwYy45NSwxOC40OSwxNC40LDMyLDMyLDMySDM0OGMxNy42NywwLDMwLjg3LTEzLjUxLDMyLTMybDIwLTMyMCIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjMycHgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMwMDA7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjMycHgiIHgxPSI4MCIgeDI9IjQzMiIgeTE9IjExMiIgeTI9IjExMiIvPjxwYXRoIGQ9Ik0xOTIsMTEyVjcyaDBhMjMuOTMsMjMuOTMsMCwwLDEsMjQtMjRoODBhMjMuOTMsMjMuOTMsMCwwLDEsMjQsMjRoMHY0MCIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjMycHgiLz48bGluZSBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6MzJweCIgeDE9IjI1NiIgeDI9IjI1NiIgeTE9IjE3NiIgeTI9IjQwMCIvPjxsaW5lIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS13aWR0aDozMnB4IiB4MT0iMTg0IiB4Mj0iMTkyIiB5MT0iMTc2IiB5Mj0iNDAwIi8+PGxpbmUgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjMycHgiIHgxPSIzMjgiIHgyPSIzMjAiIHkxPSIxNzYiIHkyPSI0MDAiLz48L3N2Zz4=);
position: absolute;
bottom: -0.75rem;
right: -0.75rem;
width: 3rem;
height: 3rem;
padding: 0.5rem;
border: 4px solid;
border-radius: 100%;
background: aliceblue var(--icon) no-repeat 50% 50% / 70%;
color: white;
cursor: pointer;
&:hover {
background-color: orangered;
}
}
.add-btn {
--icon: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI1MTIiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZS8+PGxpbmUgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjMycHgiIHgxPSIyNTYiIHgyPSIyNTYiIHkxPSIxMTIiIHkyPSI0MDAiLz48bGluZSBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6MzJweCIgeDE9IjQwMCIgeDI9IjExMiIgeTE9IjI1NiIgeTI9IjI1NiIvPjwvc3ZnPg==);
width: 3rem;
height: 3rem;
padding: 0.5rem;
border: 4px solid;
border-radius: 100%;
background: aliceblue var(--icon) no-repeat 50% 50% / 70%;
color: white;
cursor: pointer;
&:hover {
background-color: cornflowerblue;
}
}
.sr-only {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
footer {
text-align: center;
font-style: italic;
line-height: 1.42;
}
}
@layer warning {
.warning {
padding: 1em;
margin: 1em 0;
border: 1px solid #ccc;
background: rgba(255 255 205 / 0.8);
text-align: center;
}
.warning > :first-child {
margin-top: 0;
}
.warning > :last-child {
margin-bottom: 0;
}
.warning a {
color: blue;
}
.warning--info {
border: 1px solid #123456;
background: rgb(205 230 255 / 0.8);
}
.warning--alarm {
border: 1px solid red;
background: #ff000010;
}
}
Adding JavaScript Functionality
Now, let’s implement the JavaScript code to handle adding and removing cards with view transitions. This code will attach event listeners to the “Add” and “Delete” buttons, utilizing the View Transition API to create smooth animations.
document.querySelector('.cards').addEventListener('click', e => {
if (e.target.classList.contains('delete-btn')) {
if (!document.startViewTransition) {
e.target.parentElement.remove();
return;
}
e.target.parentElement.style.viewTransitionName = 'targeted-card';
document.startViewTransition(() => {
e.target.parentElement.remove();
});
}
})
document.querySelector('.add-btn').addEventListener('click', async (e) => {
const template = document.getElementById('card');
const $newCard = template.content.cloneNode(true);
$newCard.firstElementChild.style.backgroundColor = `#${ Math.floor(Math.random()*16777215).toString(16)}`;
if (!document.startViewTransition) {
document.querySelector('.cards').appendChild($newCard);
return;
}
$newCard.firstElementChild.style.viewTransitionName = 'targeted-card';
const transition = document.startViewTransition(() => {
document.querySelector('.cards').appendChild($newCard);
});
await transition.ready;
const rand = window.performance.now().toString().replace('.', '_') + Math.floor(Math.random() * 1000);
document.querySelector('.cards .card:last-child').style.viewTransitionName = `card-${rand}`;
});
Including Header Assets
For the code to work you might need some header assets for complete functionality
<meta name="viewport" content="width=device-width, initial-scale=1">
Including Footer Assets
Also for complete functionality you can add assets like js files in the footer.
Congratulations! You have successfully implemented Add/remove Cards With View Transition In Vanilla Js. This will give a better user experience when adding or removing content.







