This repo contains a small animation sample using Svelte (on top of SvelteKit). All of the significant transitions/animations are built with built-in Svelte tools and run smooth as hell.
First up, animation for the filter "pills".
Check out the filters.svelte
component for implementation.
The transition of pills from "available" to "active" effectively boils down to creating a crossfade send/receive pair (provided by Svelte!):
<script lang="ts">
import {crossfade} from "svelte/transition";
import {slide} from 'svelte/transition';
const [send, receive] = crossfade({
duration: 300,
fallback: slide
});
</script>
and then applying these crossfade animations to the entering/exiting elements, sort of like:
{#if $inactiveTags.length}
<div class="flex gap-2" transition:fadeSlide={{ duration: 300 }}>
{#each $inactiveTags as tag (tag)}
<button
in:receive={{key: tag}}
out:send={{key: tag}}
on:click={() => activeTags.addTag(tag)}
animate:flip={{duration: 300}}
class="px-5 w-24 py-1 bg-white rounded-full text-xs"
>{tag}</button>
{/each}
</div>
{/if}
The crossfade animations are applied via the in:receive
and out:send
directives! Ezpz, just need to specify a key
for each element so Svelte knows where to crossfade elements to/from.
The "active" and "available" filter sections will smoothly collapse/expand based on whether or not they have elements in them. That is handled via the transition:fadeSlide={{ duration: 300 }}
bit above, where fadeSlide
comes from a custom fadeSlide
transition. This just animates section's height/opacity to provide a collapsing/expanding effect.
Next up, the grid reordering.
You can check the source in the item-grid.svelte
component. This is really gets me fired up: all that's needed for this animation to work is incorporated into the markup below.
<div class="flex-1 overflow-y-auto">
<div class="grid grid-cols-3 gap-2 p-2 aspect-square">
{#each $availableItems as item (item.id)}
<div
class="p-6 bg-gray-100 rounded cursor-pointer aspect-square hover:shadow-lg transition-shadow duration-300"
animate:flip={{duration:d => 30 * Math.sqrt(d)}}
on:click={() => $activeItem = item}
transition:fade
>
{#if $activeItem?.id !== item.id}
<img
src={item.imgUrl}
alt="Pokemon {item.id}"
class="w-full h-full"
in:receive={{key: item.id}}
out:send={{key: item.id, delay: 300}}
/>
{/if}
</div>
{/each}
</div>
</div>
There's a fair bit of markup there, but the Svelte magic that handles the reordering animation is animate:flip={{duration:d => 30 * Math.sqrt(d)}}
. The animate:flip
directive uses the FLIP technique to animate the elements between their starting and ending positions. It's sort of... magical.
You might also notice there's a small fading effect when an item card is added or removed from the grid. Again, Svelte makes that magically simple. Just throw the transition:fade
on the div
s that are appearing/disappearing.
And finally, the item selection.
This one is a bit trickier, but not much. There's a crossfade animation between the item grid and the active item overlay.
Since there is a crossfade that is shared between two separate components, we can pull the crossfade definition out and into a shared selectedItemCrossfade.ts
file, which is then imported into the two separate components.
In the item-grid.svelte
component, we use in:receive
and out:send
directives to use our shared crossfade send
and receive
methods:
<!-- ... -->
<img
src={item.imgUrl}
alt="Pokemon {item.id}"
class="w-full h-full"
in:receive={{key: item.id}}
out:send={{key: item.id, delay: 300}}
/>
<!-- ... -->
And in the active-item.svelte
component we have a similar setup:
<!-- ... -->
<img
src={img.imgUrl}
alt="Image of pokemon {img.imgUrl}"
in:receive={{key: img.id, delay: 300}}
out:send={{key: img.id}}
/>
<!-- ... -->
The matching keys between in:receive
and out:send
is basically all Svelte needs to make the crossfade magic happen.