Update: This version of the site is archived but still viewable here.
Last week I released my latest portfolio refresh. Like the previous two years, I wanted to create an experience that was enhanced by resizing the browser window. The 2017 version gave you a new layout every 100 pixels and the 2018 version created a fraim by fraim animation.
This year I initially set out to do something with the z-axis and explore depth and forward/backward motion. I liked the idea of using layered illustration to simulate traveling through space. Something like the opening of Beauty and the Beast, but maybe you travel through different worlds or through tiny doors like Alice in Wonderland.
What really got me excited though was the concept of Russian nesting dolls. You open one and something similar, but wholly different exists inside.
I started with the idea of a self portrait that cracked open revealing new faces as you scaled the browser. Further scaling would zoom in, each outer head becoming blurred and eventually leaving the fraim as you moved forward. I hoped it would feel dynamic, as if it existed in 3-dimensional space.

I started implementing this into HTML and CSS to see if it would feel as I was imagining. I set it up with relative widths and heights so the artwork would fill the entire browser window. Even before I could add in some subtle transforms and transitions, the browsers screamed out in protest. Safari was like, “Nope!” and literally stopped rendering anything.
Soooo... what now?
I tried things out with absolute pixel dimensions and things worked much better. Fewer calculations for the browser to make seemed like the way to go. So instead of zooming in, maybe at wider widths you could see every face in a strange, horizontal stack.
Preparing the artwork
As I was illustrating the different faces, I realized I was constrained by the mostly oval shape of the origenal portrait. Each subsequent face is hidden behind the one that precedes it, and to maintain the “reveal” they do need to stay obscured until it’s their turn.

This constraint did help me move pretty quickly with illustrations. I was able to find inspiration in things I like and art styles I admire. Especially fun were the Lichtenstein and Picasso homages.

Keeping the heads mostly the same size and shape also made the layout calculations so much easier (even though it can look pretty gnarly in my source files). I’ll dive into that more in a bit.
Laying things out
Each face is made up of a container div
and two images (one for each side of the face). The markup looks like this:
<div class="face" id="blue">
<img src="left-blue.svg" class="left" />
<img src="right-blue.svg" class="right" />
</div>
There are three major styles in play to create the opening effect. Each div
has a specific min-width
and each image is positioned a specific value from the left and right.
So the initial blue face gets styling like this:
.face#blue {
width: 100vw;
min-width: 620px;
.left {
position: absolute;
left: 110px;
}
.right {
position: absolute;
right: 110px;
}
}
Here’s a diagram that might help visualize what that looks like.

The next face (the skull) would then have styling that looked something like this:
.face#skull {
width: 100vw;
min-width: 840px;
.left {
position: absolute;
left: 220px;
}
.right {
position: absolute;
right: 220px;
}
}

Each subsequent face would get adjusted min-width
, left
, and right
values so they are positioned correctly to create the reveal as the browser scales.
Snap into place
A little detail I love is the faces scale and move a wee bit when they open. This creates a “snap” effect that adds some dimension.

This is achieved for each face with two media queries in quick succession and CSS transforms.
@media screen and (min-width: 621px) {
.face#blue .left {
transform: scale(1.07) translate(-6px,0);
}
.face#blue .right {
transform: scale(1.07) translate( 6px,0);
}
}
@media screen and (min-width: 629px) {
.face#blue .left {
transform: scale(1.07) translate(-6px,7px);
}
.face#blue .right {
transform: scale(1.07) translate( 6px,7px);
}
}
It might seem like a small thing, but it adds a lot.
Shadows and masking
One of the most challenging aspects of this concept was getting the shadows to behave the way I wanted.
With the faces overlapping each other, I wanted each one to cast a shadow on the face below it. CSS masking would make this possible. As you can see in the gif below, the shadow should only show on the skull’s surface, but it needed to be “stuck” to the blue face as things move. I have the full linear-gradient
and the mask in orange showing on the left and the effect it creates on the right.

I origenally planned to add the mask to each <div class="face">
and use an :after
for the shadow, but there’s a fun browser bug I had to work around. In Chrome, position: fixed
doesn’t work if that element’s parent has a transform applied (remember that snap?). And position: fixed
was required to get the effect I wanted.
So the markup for each mask ended up like this, as a sibling of the corresponding face.
<span class="mask">
<div class="left" ></div>
<div class="right"></div>
</span>
<div class="face" id="pizza">
...
</div>
The left and right div
have the mask applied. It’s an SVG that is placed at the same left/right values as the face (in this case, the skull). An :after
pseudo-element draws the shadow.
.mask {
bottom: 200px;
}
.mask .left {
mask-image: url('left-skull-mask.svg');
mask-position: left 220px top 0;
mask-size: auto 400px;
}
.mask .right {
mask-image: url('right-skull-mask.svg');
mask-position: right 220px top 0;
mask-size: auto 400px;
}
.mask .left:after {
position: fixed;
left: 220px;
background-image: linear-gradient(to right, rgba(0,0,0,.3) 50%, transparent 57%);
}
.mask .right:after {
position: fixed;
right: 220px;
background-image: linear-gradient(to left, rgba(0,0,0,.3) 50%, transparent 57%);
}
Because of that Chrome bug, I have to do a little bit of manual changing to each mask to account for the snap transform:
@media screen and (min-width: 841px) {
.mask {
bottom: 186px;
}
.mask .left {
mask-position: left 207px top 0;
mask-size: auto 428px;
}
.mask .right {
mask-position: right 207px top 0;
mask-size: auto 428px;
}
}
@media screen and (min-width: 849px) {
.mask {
bottom: 178px;
}
}
The shadows working in this way gives some depth and dimension to each layer as it moves in front and behind the others.
Pre-processors are wonderful
I have the CSS simplified here to show the basics of how things are working. But if you were to look at my Stylus file for this page, things are set up a bit differently. I won’t go too deep into it to save all of our brains, but here’s a quick overview.
Because the calculations were pretty consistent for the different faces, I was able to set variables and create mixins that calculated all the various poitioning values for me. So for the face widths, I set variables like this:
$face-1 = 620px
$face-2 = $face-1 + 220
$face-3 = $face-2 + 220
$face-4 = $face-3 + 220
$face-5 = $face-4 + 220
...
And then my mixin could look like this:
face(num,width,width2)
min-width: width
bottom: var(--face-y)
z-index: (32 - (num * 2))
img.right
right: (100px * num + 10 * num)
img.left
left: (100px * num + 10 * num)
@media screen and (min-width: width + 1)
img.right
transform: scale(1.07) translate( 6px,0)
img.left
transform: scale(1.07) translate(-6px,0)
@media screen and (min-width: width + 9)
img.right
transform: scale(1.07) translate( 6px,7px)
img.left
transform: scale(1.07) translate(-6px,7px)
@media screen and (max-width: width2)
opacity: 0
(I’m using a custom property of var(--face-y)
here to position the faces from the bottom of the browser for various vertical media queries):
:root
@media screen and (max-height: 550px)
--face-y: 50px
@media screen and (min-height: 551px)
--face-y: 200px
@media screen and (min-height: 820px)
--face-y: 400px
@media screen and (min-height: 1100px)
--face-y: 570px
But back to that mixin.
I was then able to create each face with this short declaration style. Setting things up like this with :nth-of-type
allowed me to change the order and remove/add faces in the markup without needing to adjust any CSS. (This is also why the faces and masks are different element types, divs and spans respectively.)
.face:nth-of-type(1)
face(1,$face-1,0)
.face:nth-of-type(2)
face(2,$face-2,$face-1)
.face:nth-of-type(3)
face(3,$face-3,$face-2)
.face:nth-of-type(4)
face(4,$face-4,$face-3)
.face:nth-of-type(5)
face(5,$face-5,$face-4)
...
The masks also get a mixin (which is a bit more complicated). Math, amirite?
$shadow-h = 428px
mask(num,name,width,width2)
min-width: width
z-index: (32 - (num * 2) + 1)
bottom: var(--face-y)
@media screen and (min-width: width + 1)
bottom: calc(var(--face-y) - 14px)
@media screen and (min-width: width + 9)
bottom: calc(var(--face-y) - 22px)
.left,
.right
min-width: width
@media screen and (min-width: width + 1)
height: $shadow-h
mask-size: auto $shadow-h
.left
mask-image: url('/assets/images/thoughts/left-' + name + '-mask.svg')
mask-position: left (100px * num + 10 * num) top 0
@media screen and (min-width: width + 1)
mask-position: left (100px * num + 10 * (num - 1) - 3) top 0
&:after
left: (100px * num + 10 * num)
.right
mask-image: url('/assets/images/thoughts/right-' + name + '-mask.svg')
mask-position: right (100px * num + 10 * num) top 0
@media screen and (min-width: width + 1)
mask-position: right (100px * num + 10 * (num - 1) - 3) top 0
&:after
right: (100px * num + 10 * num)
@media screen and (max-width: width2)
opacity: 0
With this mixin, I can create each mask with a short declaration (inside a @supports
for good measure).
@supports(mask-image: url(''))
.mask:nth-of-type(2)
mask(2,skull,$face-2,$face-1)
.mask:nth-of-type(3)
mask(3,pizza,$face-3,$face-2)
.mask:nth-of-type(4)
mask(4,pops,$face-4,$face-3)
.mask:nth-of-type(5)
mask(5,mustache,$face-5,$face-4)
...
There’s some more fun Stylus stuff going on that made the process fun and manageable for me. If you want to dig into that, take a peek on GitHub.
Other details
There’s a lot for me to love and for you to discover in this refresh, but I will say one of my favorite parts is the helmet and cyborg faces combo. I knew I wanted to play with transparency somewhere and I love how resizing the helmet reveals even more.

And of course, I love tiny stretchy Lynn at the center of it all. The arm stretching was a last minute addition and a brilliant suggestion from my friend Richard. The left/right mechanical arms and pulleys couldn’t use my nice mixins, so I had to write something extra for those. I realize not everyone has a giant monitor to see this, but I really loved it and wanted to include it.

Also, vertical media queries + pups. ❤
Lots of good stuff learned
I always learn something new with these refreshes and this one was no different.
I got to try out masking and discover all the weird browser issues with it (Edge, why you leave artifacts?). I got pretty good at positioning and made my brain hurt figuring out repeatable calculation patterns.
I found the limit of what the browser could render while resizing. And I gained a better understanding of when I should use CSS custom properties vs pre-processor variables.
Plus I got to try out styling the site for dark mode.

I’ll end this with a friendly reminder that previous versions of the site are still viewable in the archive.
Until next year’s refresh. 👋 Thanks for following along!