Spring Physics in my Word Game? - by Cole Chamberlin
Cole's Substack
SubscribeSign in
Spring Physics in my Word Game?<br>Going overboard with CSS animations in Svelte
Cole Chamberlin<br>May 17, 2026
Share
This is my first in a series of technical blogs about the development of Gram Jam. This article will explain how I achieved a highly polished physics based animation engine for my word game, leveraging CSS and SvelteJS.
Gram Jam started as a fun weekend project to prove a concept idea my brother and I had cooked up in an afternoon. It was Spring 2022 and Wordle had become immensely popular and just sold to the NYT for an eye-watering amount. I like word games, and I like web development. I was also interested in trying out this new web framework Svelte that I’d been hearing rave reviews of. The idea was simple, mash together Scrabble and Bejeweled (or Candy Crush depending on your age) into a solo word puzzle.<br>Thanks for reading Cole's Substack! Subscribe for free to receive new posts and support my work.
Subscribe
The above recording is what the game looks like today, but this is the result of years of bending the Svelte framework and refining the UX. The first few versions were not quite as pretty.
The first prototype came together in about 48 hours. Coming from React, Svelte had an intuitive reactivity model and came with some great animation utilities out of the box. In the above recording, you can see the FLIP (first, last, invert, play) effect used to swap two tiles on the board. This is as simple as:
const tiles = [...];
{#each tiles as tile (tile.id)}
{/each}
Ok, not quite that simple, there’s a lot of positioning and styling that I’m glossing over. But Svelte’s directives made React look like a clunker in comparison. I’d used a number of React libraries in the past to achieve FLIP animations and they all seemed to have their caveats. This was baked right into the framework!
You’ll also notice in that recording that the tiles have a subtle pop-in transition when they spawn on the board. This is achieved via Svelte’s in/out directives:
// the function signature for a simple Svelte transition<br>export function fade(node, { delay = 0, duration = 400, easing = linear } = {}) {<br>const o = +getComputedStyle(node).opacity;<br>return {<br>delay,<br>duration,<br>easing,<br>css: (t) => `opacity: ${t * o}; scale: `<br>};
// a slightly more complicated transition<br>export function scale(<br>node,<br>{ delay = 0, duration = 400, easing = cubic_out, start = 0, opacity = 0 } = {}<br>) {<br>const style = getComputedStyle(node);<br>const target_opacity = +style.opacity;<br>const transform = style.transform === 'none' ? '' : style.transform;<br>const sd = 1 - start;<br>const od = target_opacity * (1 - opacity);<br>return {<br>delay,<br>duration,<br>easing,<br>css: (_t, u) => `<br>transform: ${transform} scale(${1 - sd * u});<br>opacity: ${target_opacity - od * u}<br>};
Svelte has a handful of common transitions that are great defaults, but you can go wild with arbitrary easing and CSS styles. One nice thing about using CSS for this instead of JS is modern browsers can hardware accelerate these animations, running them outside of the main execution thread so they don’t compete with the game logic for CPU cycles. You can read more about Svelte transitions here.<br>It would be nice if instead of the new tiles popping in at the final x/y position, they instead fell down as if the board had an endless feed of new tiles off screen.
We can use the in:fly transition to give the dropping in effect. This looks a lot better. (You’ll notice the final transitions are even smoother, this came a lot later in development. I’ll explain the final effect later down). The other thing you may have noticed in this recording is the word now smoothly animates from its position on the board to the shelf above it.
This is a crossfade, the third type of svelte animation directive that I leveraged. While FLIP is used for moving an element within its container, and in/out transitions are for entering and exiting, crossfade is for when you want to move an element from one container to another. As the name implies, the effect creates two animations, one playing from the starting position and one playing in reverse from the ending position, and fades between them. Here’s what it looks like in code:<br>/// animations.svelte.ts<br>import { crossfade } from 'svelte/transition';
export const [send, receive] = crossfade();
/// Board.svelte
{#each tiles as tile (tile.id)}
{/each}
/// Word.svelte
{#each word as tile}
{/each}
The “key” here is that the two elements on the page share a send/receive pair, and are linked via the tile.id. Then in the game logic when a word is detected, the tiles on the board are updated so the matched word is removed, and the word in the container is updated in the same “tick”. Svelte detects these two changes and reconciles them to determine the starting and ending positions of the tiles. I’m skimming over the game logic, that is a subject for another article.<br>One final effect...