Trello Checkbox Animation
One of my favorite guys in web development is Sam Selikoff. Some weeks ago he posted this video where he created Trello's fancy checkbox animation that is shown when you complete the last task in the list. He's using ReactJS and the Framer Motion animation library which is unfortunately only available for ReactJS.
Since I love to use VueJS, I wanted to re-build this animation using this framework but I had to use something different than Framer Motion. I researched a bit and tried the popular GSAP animation library and here's the result:
Demo
Checklist
Code
The icons I used are part of the great Phosphor Icons project. There are many framework/library based integrations available, for example for VueJS, ReactJS, Svelte and some others - really great!
<template>
<div class="flex w-full max-w-xs flex-col rounded-lg bg-gray-100 p-4 shadow-2xl">
<div class=" flex items-center gap-x-2">
<PhListBullets class="text-xl -ml-0.5 text-gray-600" />
<h3 class="font-semibold !text-base !text-slate-700 !my-0">
Checklist
</h3>
</div>
<div class="mt-4 flex flex-col gap-y-3">
<label
v-for="item in items"
:key="item.id"
class="text-sm flex w-full cursor-pointer items-center"
:class="{ 'line-through text-gray-500': item.checked }"
>
<input
type="checkbox"
class="hidden"
@change="handleChange(item.id)"
>
<span
class="checkbox inline-flex items-center justify-center w-4 aspect-square mr-3 rounded-sm border-2"
:class="[
item.checked
? 'bg-[#0079bf] border-[#0079bf] hover:opacity-70'
: 'bg-[#fff] border-[#dfe1e6] hover:bg-gray-100'
]"
>
<PhCheck
v-if="item.checked"
weight="bold"
class="text-white h-auto"
/>
</span>
{{ item.title }}
</label>
</div>
</div>
</template>
<script setup>
import gsap from 'gsap'
import { PhCheck, PhListBullets } from '@phosphor-icons/vue/compact'
const items = ref([
{ id: 1, title: 'One', checked: true },
{ id: 2, title: 'Two', checked: true },
{ id: 3, title: 'Three', checked: true },
{ id: 4, title: 'Four', checked: false },
{ id: 5, title: 'Five', checked: true },
{ id: 6, title: 'Six', checked: true },
{ id: 7, title: 'Seven', checked: true },
])
function handleChange(id) {
items.value = items.value.map((item) => ({
...item,
checked: item.id === id ? !item.checked : item.checked,
}))
if (items.value.every(item => item.checked)) {
const lastCompletedItem = items.value.findIndex((item) => item.id === id)
const random = Math.random()
if (random < 1 / 3) {
// bounce
gsap.to('.checkbox', {
keyframes: { scale: [1, 1.25, 1] },
duration: 0.35,
stagger: {
from: lastCompletedItem,
each: 0.075,
},
})
} else if (random < 2 / 3) {
// shimmy
gsap.to('.checkbox', {
keyframes: { x: [0, 2, -2, 0] },
duration: 0.4,
stagger: {
from: lastCompletedItem,
each: 0.1,
},
})
} else {
// shake
gsap.to('.checkbox', {
keyframes: { rotate: [0, 10, -10, 0] },
duration: 0.5,
stagger: {
from: lastCompletedItem,
each: 0.1,
},
})
}
}
}
</script>
Follow-Up
I wanted to be close to Sam's example so it's easier for you to spot the differences. Now it's up to you to improve the code! Use Vue's v-model
for the input
or create configuration objects for the different animation styles. I would love to see you sharing your code with me on Twitter.