Add/Remove Cards With View Transition in Vanilla JS

Add/Remove Cards With View Transition in Vanilla JS
Code Snippet:Add/Remove Cards with View Transitions (using view-transition-classs)
Author: Bramus
Published: March 22, 2024
Last Updated: March 22, 2024
Downloads: 311
License: MIT
Edit Code online: View on CodePen
Read More

This code Vanilla JS code snippet helps you to create functionality to add/remove cards with view transition. When a delete button is clicked, the corresponding card smoothly disappears from view. Similarly, when adding a new card, it smoothly transitions into view with a random background color. These transitions enhance the user experience when managing cards on a webpage.

You can use this code on any webpage to add or remove items dynamically. It provides smooth transitions, enhancing visual appeal and user experience. With Vanilla JavaScript, it’s lightweight and doesn’t require additional libraries.

How to Add/Remove Cards With View Transition in Vanilla JS

1. Start by setting up your HTML structure. Create elements for the cards and buttons. Ensure each card has a delete button associated with it.

<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>You browser does not support <code>view-transtion-class: &lt;custom-ident&gt;+</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>

2. Next, define CSS styles for your cards, buttons, and any transitions you want to apply. You can customize the appearance according to your design preferences.

@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;
		}
	}

	/* 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;
		}
	}

}

/* Etc. */
@layer base {
	body {
		display: grid;
		height: 90dvh;
		place-items: center;
		padding: 2rem 0;
		font-family: system-ui, sans-serif;
	}

	.cards {
		padding: 0;
		display: flex;
		justify-content: center;
		width: 100%;
		gap: 2rem;
		padding: 1rem;
		flex-wrap: wrap;
	}

	.card {
		width: 100%;
		aspect-ratio: 2/3;
		display: block;
		position: relative;
		border-radius: 1rem;
		max-width: 220px;
		min-width: 100px;

		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 {
		box-sizing: border-box;
		padding: 1em;
		margin: 1em 0;
		border: 1px solid #ccc;
		background: rgba(255 255 205 / 0.8);
	}

	.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;
	}
}

3. Now, write JavaScript logic to handle adding and removing cards with view transitions. Use event listeners to detect clicks on the add and delete buttons.

document.querySelector(&singleQuote;.cards&singleQuote;).addEventListener(&singleQuote;click&singleQuote;, e => {
	if (e.target.classList.contains(&singleQuote;delete-btn&singleQuote;)) {

		if (!document.startViewTransition) {
			e.target.parentElement.remove();
			return;
		}

		e.target.parentElement.style.viewTransitionName = &singleQuote;targeted-card&singleQuote;;
		document.startViewTransition(() => {
			e.target.parentElement.remove();
		});
	}
})

document.querySelector(&singleQuote;.add-btn&singleQuote;).addEventListener(&singleQuote;click&singleQuote;, async (e) => {
	const template = document.getElementById(&singleQuote;card&singleQuote;);

	const $newCard = template.content.cloneNode(true);

	if (!document.startViewTransition) {
		document.querySelector(&singleQuote;.cards&singleQuote;).appendChild($newCard);
		return;
	}

	$newCard.firstElementChild.style.viewTransitionName = &singleQuote;targeted-card&singleQuote;;
	$newCard.firstElementChild.style.backgroundColor = `#${ Math.floor(Math.random()*16777215).toString(16)}`;
	const transition = document.startViewTransition(() => {
		document.querySelector(&singleQuote;.cards&singleQuote;).appendChild($newCard);
	});

	await transition.finished;

	const rand = window.performance.now().toString().replace(&singleQuote;.&singleQuote;, &singleQuote;_&singleQuote;) + Math.floor(Math.random() * 1000);
	document.querySelector(&singleQuote;.cards .card:last-child&singleQuote;).style.viewTransitionName = `card-${rand}`;

});

That’s all! hopefully, you have successfully created functionality to add/remove cards on your website. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

About CodeHim

Free Web Design Code & Scripts - CodeHim is one of the BEST developer websites that provide web designers and developers with a simple way to preview and download a variety of free code & scripts. All codes published on CodeHim are open source, distributed under OSD-compliant license which grants all the rights to use, study, change and share the software in modified and unmodified form. Before publishing, we test and review each code snippet to avoid errors, but we cannot warrant the full correctness of all content. All trademarks, trade names, logos, and icons are the property of their respective owners... find out more...

Please Rel0ad/PressF5 this page if you can't click the download/preview link

X