Update: This version of the site is archived but still viewable here.
Another long one, so here’s some jump links if you’re looking for something specific:
Concept and inspiration
For 2023 I was hoping to do some experimenting with view transitions, but they didn’t seem quite ready for me yet. So the ol’ thinking cap went back on.
I’ve had a couple responsive ideas floating around in my head for a few years. The first was maybe you could get some version of the site only while you’re resizing the browser. It could be a hidden world briefly revealed if you know to resize (and luckily I’ve been encouraging people to do that for years). The second was maybe you could get different versions of the site depending on whether you were resizing the browser larger or smaller. It opens up some cool possibilities if a growing site doesn’t have to be just the reverse of a shrinking site.
Both of these ideas require a bit of JavaScript. I needed the site to know when it’s actively being resized and whether it’s growing or shrinking in that moment.
A developer bud of mine helped me get started with ResizeObserver
. I’ll go though the specific code I ended up with farther down, but ultimately I wanted to toggle a class of active
while resizing and swap classes shrinking
and growing
depending on the direction.
I ran into a hiccup pretty quickly. Turns out resizing is very literal and any tiny pause would cause the class to toggle. The effect was super jarring and jittery. I could’ve probably added some mousedown/mouseup events to make the resize “end” only when you lifted your mouse. That felt tricky to me since windows can resize without a mouse and truthfully I didn’t want to explore all the ways that happens!
So I opted to have things reveal on the first resize and remain visible until you navigate away or refresh.
With that decided, I started experimenting with what the shrinking
and growing
sites could be. I soon ran into another issue. The more different the two sites were, the less impactful the transition between them felt. It was almost like my 2017 refresh but if it only had two layouts instead of 21. This really wasn’t the effect I was going for.
The sites had to be tied together in some way, similar but different, with something to visually ground them. So it got me thinking about film transitions and match cuts. Edgar Wright is so good at them.
It also got me thinking about multiverses. Like how in the show Fringe, match cuts and lens flare transitions showed us we were moving between the two universes.
These cuts sometimes move us from earlier to later in time or from one place to another. That concept led me to the idea of a city street: one normal and one grungy after some apocalyptic event. I liked how they could be either present/future or parallel versions of the same place.
Street scenes: growing and shrinking
In case you haven’t interacted with this version of the site, this is how the artwork ultimately looks and behaves. (Try resizing it on a wide monitor!)
The street scene is a single SVG exported from Illustrator. The way the file is configured in Illustrator does most of the work here. Essentially there are four layers to the artwork:
- default (artwork that persists between shrinking and growing)
- shrinking (grunge street only)
- growing (normal street only)
- default-top (also persists between the two, but needs to be on top of the growing/shrinking artwork)




When I export from Illustrator, I use layer names as IDs and that gives me SVG code like this (simplified here):
<svg>
<g id="default">...</g>
<g id="shrinking">...</g>
<g id="growing">...</g>
<g id="default-top">...</g>
</svg>
The JavaScript looks like this for adding classes shrinking
and growing
to the <html>
element. This could probably use some refactoring from a JS professional, but alas it works and so I move on.
let oldWidth = -1;
const myObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
const newWidth = entry.contentRect.width;
if (oldWidth !== -1 && oldWidth > newWidth) {
// Shrinking
root.classList.remove("growing");
if (!root.classList.contains("shrinking")) {
root.classList.add("shrinking");
}
} else if (oldWidth !== -1 && oldWidth < newWidth) {
// Growing
root.classList.remove("shrinking");
if (!root.classList.contains("growing")) {
root.classList.add("growing");
}
}
oldWidth = newWidth;
});
});
myObserver.observe(body);
And from here, I hide/show the shrinking
and growing
SVG layers depending on the root class.
#growing,
#shrinking {
opacity: 0;
}
.growing #growing,
.shrinking #shrinking {
opacity: 1;
}
Not much going on code-wise as long as the artwork is properly set up, which takes a bit more work! The biggest challenge was theming and changing the colors in the artwork.
Theming, also described as why am I doing this?
To give the scenes proper moods, growing
and shrinking
each have their own color themes. The normal street gets a warmer, brighter theme with purples and pinks, while the grunge street gets a colder, gloomier theme with browns and blues.
But also whoops there’s light and dark modes to consider. I do like the idea that the streets could be shown in daylight and at nighttime. But this meant there were four color themes to manage. And I was committed to the one SVG handling them all.

So I set up the themes like this with each color corresponding with similar colors in the other themes.

This allowed in most cases (I’ll get back to that) for a CSS custom property (variable) to handle the color swaps. I’d edit the SVG code so strokes and fills use these variables:
<path stroke="var(--color1)">...</path>
<path fill="var(--color2)">...</path>
<path fill="var(--color3)">...</path>
And the CSS could look like this:
/* light mode */
:root.shrinking {
--color1: #fff;
--color2: #f4eddf;
--color3: #d8cfb8;
}
:root.growing {
--color1: #fff;
--color2: #fcf4df;
--color3: #efd2bb;
}
/* dark mode */
@media (prefers-color-scheme: dark) {
:root.shrinking {
--color1: #e2b788;
--color2: #d3a97a;
--color3: #8e7c5f;
}
:root.growing {
--color1: #f69f72;
--color2: #eb7162;
--color3: #a55164;
}
}
All the colors would eventually get added here and the illustration would adapt to the changing themes.
To make this as easy as possible, the SVG that gets exported from Illustrator is set up with everything in light mode shrinking
(even the growing
only layer).

growing
never showing up that way on the site)This limited things to just one set of hex color codes to find and replace with variables. I use RegReplace in Sublime Text for this. There’s a lot to be desired from design software exporting and this SVG export wishlist I wrote up is super relevant to what I did with this project.
Let’s make it more complicated
So... what about those edge cases that the CSS variables couldn’t cover? Well, sometimes the direct color swap just didn’t work when going from light to dark mode, usually too much contrast or not enough. The shop windows are a good example. For the growing
street’s light mode, I wanted the windows to be a blue tint reflecting the sky. But for dark mode, I wanted the windows to look like they’re being illuminated from within.
So the colors I wanted were #7 for light mode and #2 for dark.

So I’d name the layers that needed this specific color change L7D2
in Illustrator. Light 7 to Dark 2. These would get exported as IDs, I’d change them to classes like this:
<path class="L7D2" fill="var(--color7)">...</path>
And then the CSS would look like this:
@media (prefers-color-scheme: dark) {
.L7D2 {
--color7: var(--color2);
}
}
And so on for each of the swaps that was needed. Since the artwork is set up in light mode, I only need to change the values for dark mode. It was a bit of a brain melter as I was working on it, but once I got the system down it worked pretty well.
We ride at dawn
One last bit about theming! When you first get to the site (on any page), the theme is just black and white until you start to resize. Go ahead and try resizing this page.

These initial colors are set with variable fallbacks. I call this initial state of the site “The Dawn” so I set those colors like this:
/* light mode */
:root {
--dawn-light: #f7f7f7;
--dawn-dark: #0c0c0c;
--bg-dawn: var(--dawn-light);
}
/* dark mode */
@media (prefers-color-scheme: dark) {
:root {
--bg-dawn: var(--dawn-dark);
}
}
And then variables for site components get set like this:
/* light mode */
:root {
--nav-bg: var(--color2, var(--bg-dawn));
}
/* dark mode */
@media (prefers-color-scheme: dark) {
:root {
--nav-bg: var(--color4, var(--bg-dawn));
}
}
Or for some of the SVGs, they look like this:
<path fill="var(--color3, var(--bg-dawn))">...</path>
Until variables like --color3
are activated with the growing
and shrinking
classes, we get The Dawn fallbacks.
Let’s get walking
To ground the street scenes more, I decided to have a person walking through both of them. I hoped it would provide some visual focus while making the scene more dynamic.
The walking cycle is a frame animation and the SVG sprite looks like this:

A .walking
container element is placed in the center of the viewport with its overflow: hidden
. The SVG sprite is sized appropriately and is positioned within the container.

A CSS animation moves the sprite to the left to create a stepped, walking animation:
@keyframes walking {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(-100%, 0);
}
}
.walking svg {
animation: walking 1100ms steps(10, end) infinite;
}
And that looks something like this:
But because I only want the walking animation to happen while the browser is resizing, I have to do a couple things. Earlier I mentioned that triggering something only on resize was a jittery mess, so I ended up adding a class of active
when resizing starts and setting a timeout
to remove the class after a bit of time (500ms).
const app = document.querySelector('.header-main');
const observerDebouncers = new WeakMap;
let oldWidth = -1;
const myObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
clearTimeout( observerDebouncers.get( entry.target ));
observerDebouncers.set( entry.target, setTimeout(() => {
entry.target.dispatchEvent( new CustomEvent( 'resized' ));
}, 500));
const newWidth = entry.contentRect.width;
if (oldWidth !== -1 && oldWidth > newWidth) {
// Shrinking
app.classList.add("active");
root.classList.remove("growing");
if (!root.classList.contains("shrinking")) {
root.classList.add("shrinking");
}
} else if (oldWidth !== -1 && oldWidth < newWidth) {
// Growing
app.classList.add("active");
root.classList.remove("shrinking");
if (!root.classList.contains("growing")) {
root.classList.add("growing"); }
}
oldWidth = newWidth;
});
});
body.addEventListener( 'resized', event => {
app.classList.remove("active");
});
myObserver.observe(body);
This allows enough time for the walk cycle to continue through short pauses and feel smooth if you resize the browser at a slower pace.
And when you stop resizing completely, the sprite returns to its default frame (bottom left) of just standing and waiting.
.walking svg {
transform: translate(0, -50%);
}

A few more details
To swap the direction of the walking animation from right to left, we apply a little transform:
.shrinking .walking {
transform: scale(-1, 1);
}
The walking SVG also gets the growing
and shrinking
treatment just like the street scene where layers are revealed/hidden and colors are changed.

And finally to complete the walking visual, the street SVG gets the Typetura treatment. I wrote about using Typetura to animate on resize in my 2021 case study, and here I’m using it to move the street scene to the left and right while you resize the browser.
Transitions and performance
Earlier I mentioned taking inspiration from the tv show Fringe. When they move between the two universes, along with the color change and lens flare, there’s a bit of a zoom-in-out shaky transition.
I tried something like this! I had an animation run when you switched resize directions that did a nice little scale()
transform and a saturation filter
. The browsers did not like this one bit and it slowed things down so much it wasn’t going to work.
So I experimented with just using filters and blend modes, then just fill/color transitions with different easing and delays. None seemed to feel or work quite right!
I then got thinkin’ about WandaVision and static/interference and old-school televisions. So I added a simple glitch
layer above the artwork that shows for 200ms when the direction changes. This ended up being the best compromise of achieving that visual cue and not reducing performance too much.

The entire street scene and walking animation works pretty well in all major browsers I tried. Without hardware acceleration things get a bit more choppy, but still works ok.
Things did start to get slow and stuck when the browser window was tall. The wider it got didn’t seem to matter until the window was taller than about 900px
. This forced me to move my projects list from the homepage to a dedicated /work page. I also added a little warning in the corner for those with tall browser windows. 😄

Anything else?
This one was a lot of fun, even though it felt way too complicated while in the middle of it. Overall I really like the effect and I hope it’s fun for people to discover the street scenes either on purpose or accidentally.
I used CSS custom properties more than ever before which was fun and I got to use text-wrap: balance
on the big headlines on Thoughts pages which is extremely cool. A small thing we’ve been wanting forever.
I still haven’t gotten around to moving the site off Grunt, but I hope to do that sometime this year. She tells herself!
Crossing my fingers for view transitions being ready for the next one of these.
Thanks for reading! 👋 See you next year.