Reputation: 341
I would like to animate a header once the landing page of my SvelteKit app is loaded, causing each letter of the heading to appear one slightly after the other, but all the solutions I've found for this either require some things that feel hacky and make TypeScript upset or result in layout shift.
<h1>
<span>C</span><span>A</span><span>T</span>
</h1>
I'm hoping to see if there's a more "Svelte-y" way of achieving the effect without negative side-effects.
In vanilla JS, I would simply use css to set the <h1>
element's opacity to zero and give its opacity a transition value. Then I would define a class that sets opacity to 1. In my script, I would my set an interval on page load that incrementally adds a class to the <span>
elements within the heading. That way, each letter would appear in succession and fade into view, and there would be no layout shift.
Transferring the above vanilla JS approach to Svelte introduces a few weird things:
root
) in order to query things inside it (seems hacky, so there's got to be something I don't understand; normally I would just use document.querySelectorAll()
, but that apparently isn't the way to do it in Svelte).root
variable, but I wouldn't begin to know what type to assign it.
<script>
import { onMount } from "svelte";
let root;
const lettersWithClasses = { C: 'red', A: 'green', T: 'orange' };
const animateHeading = function(letters) {
letters.forEach((span, i) => {
setTimeout(() => {
span.classList.add("viz")
}, i * 400 + 1000);
});
}
onMount(() => {
let headingLetters = root.querySelectorAll("#heading > span");
animateHeading(headingLetters);
});
</script>
<div bind:this={root}>
<h1 id="heading" class="h1 text-center text-[12rem] font-extrabold">
{#each Object.entries(lettersWithClasses) as [key, value], i}
<span
class="{value}"
>{key}</span>
{/each}
</h1>
</div>
<style>
h1 > span {
opacity: 0;
transition: opacity 1s ease-in;
}
.viz {
opacity: 1 !important;
}
.red { color: red; }
.green { color: green; }
.orange { color: orange; }
</style>
This seems like a more Svelte-y way of achieving my goal (see use of the fade
Svelte transition), but it results in layout shift :(
Also, it seems really hacky the way I'm changing the value of this ready
variable via onMount
but it's necessary in order to get the transition to run once the page is loaded (maybe it shouldn't be considered "hacky" though since Rich Harris is who suggested this method).
What is for sure is that layout shift = bad
<script>
import { onMount } from "svelte";
import { fade } from "svelte/transition";
const lettersWithClasses = { C: 'red', A: 'green', T: 'orange' };
let ready = false;
onMount(() => ready = true);
</script>
<div>
{#if}
<h1 id="heading" class="h1 text-center text-[12rem] font-extrabold">
{#each Object.entries(lettersWithClasses) as [key, value], i}
<span
in:fade|global={{ delay: 1000 + i * 400, duration: 1000 }}
class="{value}"
>{key}</span>
{/each}
</h1>
{/if}
</div>
<style>
.red {
color: #f70702;
}
.green {
color: #398c31;
}
.orange {
color: #f27202;
}
</style>
I've thought about adding a hard-coded <span>
after the closing {/each}
tag and giving it a class that makes it have a visibility of "hidden", but that seems hacky to me too.
Would it be better to just use CSS keyframes to do all this? By "better" I mean both less complicated for me as the developer and less computationally expensive for the client. Or is there a "better" way that is sort of more baked-in to Svelte?
Just assign each <span>
a different class and then @keyframes and animation to run the transition. Btw, this CSS method was the only method to fetch a perfect 100 lighthouse score in all categories.
<script>
const lettersWithClasses = { C: 'red', A: 'green', T: 'orange' };
</script>
<div>
<h1 id="heading" class="h1 text-center text-[12rem] font-extrabold">
{#each Object.entries(lettersWithClasses) as [key, value], i}
<span
class="{value}"
>{key}</span>
{/each}
</h1>
</div>
<style>
@keyframes fadeinto-red {
from { opacity: 0; color: #white; }
to { opacity: 1; color: red; }
}
@keyframes fadeinto-green {
from { opacity: 0; color: white; }
to { opacity: 1; color: green; }
}
@keyframes fadeinto-orange {
from { opacity: 0; color: white; }
to { opacity: 1; color: orange; }
}
span.red{
animation: fadeinto-red 1.8s ease-in-out both;
}
span.green{
animation: fadeinto-green 1.8s ease-in-out 0.4s both;
}
span.orange{
animation: fadeinto-orange 1.8s ease-in-out 0.8s both;
}
</style>
Upvotes: 3
Views: 913
Reputation: 7721
This would be a modification of your first example that
style:
directive<script>
const lettersWithClasses = { C: 'red', A: 'green', T: 'orange' };
function fadeIn(spanElement, index) {
setTimeout(() => {
spanElement.style.opacity = 1
}, index * 400 + 1000);
}
</script>
<div>
<h1 id="heading" class="h1 text-center text-[12rem] font-extrabold">
{#each Object.entries(lettersWithClasses) as [letter, color], index}
<span
style:color={color}
use:fadeIn={index}
>
{letter}
</span>
{/each}
</h1>
</div>
<style>
h1 > span {
opacity: 0;
transition: opacity 1s ease-in;
}
</style>
The pure CSS version with @keyframes animation could be made more universal with the use of CSS custom properties which I guess is then the cleaner an preferable way over the js action solution
<script>
const lettersWithClasses = { C: 'red', A: 'green', T: 'orange' };
</script>
<div>
<h1 id="heading" class="h1 text-center text-[12rem] font-extrabold">
{#each Object.entries(lettersWithClasses) as [letter, color], i}
<span
style="--end-color: {color}; --delay: {i*400}ms;"
class="animate"
>{letter}</span>
{/each}
</h1>
</div>
<style>
@keyframes fadeinto {
from { opacity: 0; color: white; }
to { opacity: 1; color: var(--end-color); }
}
.animate {
animation: fadeinto 1.8s ease-in-out var(--delay) both;
}
</style>
Upvotes: 2