Problem
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.
Simplified Structure of heading I'm wanting to animate
<h1><span>C</span><span>A</span><span>T</span></h1>
Solutions so far:
- "Translation" of how I would do it in vanilla JS (seems hacky)
- Svelte transitions (causes layout shift)
- pure CSS (seems like the simplest and most effective to me)
I'm hoping to see if there's a more "Svelte-y" way of achieving the effect without negative side-effects.
How I would do this in vanilla JS
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.
Svelte "translation" of above approach makes TypeScript upset
Transferring the above vanilla JS approach to Svelte introduces a few weird things:
- I have to bind the Component's top-most element to a variable (
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 usedocument.querySelectorAll()
, but that apparently isn't the way to do it in Svelte). - TypeScript gets mad at the
root
variable, but I wouldn't begin to know what type to assign it.
Component's Code
<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>
Using Svelte Transitions (causes layout shift)
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.
Just CSS
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?
example of how to do all this with CSS
Just assign each <span>
a different class and then @keyframes and animation to run the transition.
<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>