Update: This version of the site is archived but still viewable here.
This is a long one, so here’s some jump links if you’re looking for something specific:
- Folding header proof of concept
- Creating the header artwork
- Details make the illusion
- Pull-tabs
- About page changing portraits
Over the past few years, my portfolio’s homepage has served as a playground where I can experiment with using the browser in creative ways.
In 2017, I explored the idea that a site doesn’t need to look the same on every device or for every person by giving it a new layout every 100 pixels. In 2018, I tried using the browser as an animation compiler. And in 2019, I treated the browser as a physical space, able to contain more and more as it grows.
This time around I knew I wanted to continue this exploration, but I also wanted to reunify the homepage with the rest of the site for a more cohesive experience. I decided to use more expected layout conventions like a decorative header, full-width but separated content sections, and regular old scrolling.
But how to make that exciting (both to design/build and to ultimately view)?
Some inspiration
Earlier this year, I’d been experimenting with paper effects. I created a handful of pens on CodePen that (with a containing <div>
and some styling) could turn an image into a folded poster, leaning cards, a coffee table book, or a trapper keeper.

And I contributed to Style Stage with a stylesheet that turned the page into a folded paper instruction manual.

I liked the idea of treating page elements like paper: light but rigid, foldable, and layered to make cool effect. In the physical world, this is most magically realized in pop-up books.
So I went down a rabbit hole of pop-up folding techniques and paper construction. I watched tons of videos on The Pop-Up Channel on YouTube where Duncan Birmingham talks through and demonstrates every pop-up technique you can imagine. It’s amazing.

I immediately knew I wanted to do pull-tabs. The dissolve technique seemed like a fun challenge to accomplish with code (more on that later). The header felt like a good spot to recreate folding pop-up mechanics. So I dug in.
After some frustrated blank-stare-at-the-screen days, I remembered those cool Al Jaffee fold-ins from Mad Magazine and then remembered a cool demo from developer Thomas Park who recreated the fold-in effect in CSS. So neat.

Thomas’s technique uses 3D transforms on hover, but I wanted the fold to happen slowly as you resize the browser window. So there were two things to figure out: how to make the fold feel realistic and writing a sentence that made sense as words disappeared at two separate fold points.
A proof of concept
I started in CodePen, using some same-sized images to figure out the fold mechanics. Here’s the original pen if you want to tinker, but I’ll describe what’s happening here.
See the Pen 2d891f02e444c86b13e7448a3ef10242 by Lynn Fisher (@lynnandtonic)on CodePen.
Because I wanted the header to fold two separate times, there are three .panel
containers with .left
and .right
children, each containing an image. The markup looks like this:
<div class="container">
<div class="panel">
<div class="left">
<img src="tahani.jpg" />
</div>
<div class="right">
<img src="jason.jpg" />
</div>
</div>
<div class="panel">
<div class="left">
<img src="michael.jpg" />
</div>
<div class="right">
<img src="eleanor.jpg" />
</div>
</div>
<div class="panel">
<div class="left">
<img src="chidi.jpg" />
</div>
<div class="right">
<img src="janet.jpg" />
</div>
</div>
</div>
This uses the technique from my 2019 refresh with a few modifications. I originally thought I could use flexbox alone for this (which got things about 97% of the way there), but to eliminate small but unacceptable-to-me differences cross-browser, using positioning worked best.
For the first .panel
, the .left
and .right
are given a width and positioned absolutely to the left and right of the container.
.panel:first-child {
.left,
.right {
width: 210px
position: absolute;
}
.left {
left: 0;
}
.right {
right: 0;
}
}
For the next .panel
, we want the .left
and .right
to be positioned adjacent to but inbetween the first set. That looks like this:
.panel:nth-child(2) {
.left {
left: 210px;
}
.right {
right: 210px;
}
}
In the 2019 version, each image kept its width and created an overlapping effect. But here, I want the images to squish/stretch to simulate folding. So the .left
and .right
should fill the availabile space until they reach their full width. We can do this by using calc()
.
The calculation takes 100% width of the container, subtracts the width of the first two panels (210px * 2 = 420px), and then divides the space by two (since we have a .left
and a .right
to account for).
I’m using <img>
in this example, but in the site I’m using inline SVG. So each of those need preserveAspectRatio="none"
added to make sure they scale with their container.
.panel:nth-child(2) {
.left,
.right {
width: calc((100% - 420px) / 2);
max-width: 210px;
}
}
Here’s a diagram that might help visualize what’s going on.

This same treatment is applied to the third panel and its children.
(You might be thinking that the .panel
containers aren’t really needed, and you’re right! But the organizational clarity for me felt worth this extra bit of markup.)
To make it feel more “foldy”, the panels are skewed using transform: skewY()
. Setting transform-origin
and a bit of translate
helps ensure the edges connect at the right places.
.panel:first-child {
.left {
transform: skewY(-2deg);
transform-origin: right bottom;
}
.right {
transform: skewY(2deg);
transform-origin: left bottom;
}
}
.panel:nth-child(2) {
.left {
transform: skewY(-5deg);
transform-origin: left bottom;
}
.right {
transform: skewY(5deg);
transform-origin: right bottom;
}
}
.panel:nth-child(3) {
.left {
transform: skewY(-5deg) translateY(18px);
transform-origin: left bottom;
}
.right {
transform: skewY(5deg) translateY(18px);
transform-origin: right bottom;
}
}
It ultimately creates this effect:

It seems like perspective
or 3D transforms make sense here, but they were much more dynamic on resize than I wanted. skew
provides the dimensional illusion without shifting as the width changes.
Creating the artwork
So once I got the basics of the header figured out, I jumped into making text and artwork that could fold and unfold, creating a complete picture at each folding point. This was a challenge!
I tried big text, small text, splitting words in the middle (and then doing lots of searches like “words that end with -ign”), and was finally able to land on something that worked.
It was a process of trial and error, but what really helped was laying out the panels in Illustrator like this:

I would create the first panels (smallest fold) and then insert the next panels in between. And then do it again for the final fold:

The illustrations worked similarly, splitting the panels and then making sure the edges meet to create a complete image. Here’s what the final artwork looks like:

After a happy dance of finally figuring it out, I made folding artwork for the /web and /art pages too. (The art page header has a bit of a different treatment, which I’ll describe in the next section.)

Adding details
With any illusion, the details make or break it. With the folding header, the skew (as mentioned earlier) and some shading do the heavy lifting.
Each panel section gets an :after
pseudo-element that controls how much “shadow” it gets as the browser resizes. It is first sized and positioned directly on top of its parent.
.left:after,
.right:after {
content: '';
width: 100%;
height: 100%;
display: block;
position: absolute;
left: 0;
top: 0;
}
Then each panel gets some background treatment. The top gradient creates a “fold” highlight at the edge and the following two provide some shadowing on either side of the panel. Here’s an example for one section:
.left:after {
background-color: rgba(0,0,0,.7);
background-image: linear-gradient(to right, rgba(255,255,255,.2) 2px,
rgba(255,255,255,0) 2px),
linear-gradient(to right, rgba(0,0,0,0) 2px,
rgba(0,0,0,.9) 2px,
rgba(0,0,0,0) 40%),
linear-gradient(to left, rgba(0,0,0,.9),
rgba(0,0,0,0) 40%);
}
With similar styling applied to all the panels, we get something like this:

To mimic the changing light as it folds, I set a bunch of media queries in quick succession to adjust the opacity of the :after
and transition between them.
.left:after {
transition: opacity 150ms ease-out;
}
@media screen and (min-width: 501px) {
.left:after { opacity: 1; }
}
@media screen and (min-width: 550px) {
.left:after { opacity: .9; }
}
@media screen and (min-width: 600px) {
.left:after { opacity: .8; }
}
@media screen and (min-width: 650px) {
.left:after { opacity: .7; }
}
@media screen and (min-width: 700px) {
.left:after { opacity: .6; }
}
@media screen and (min-width: 750px) {
.left:after { opacity: .5; }
}
@media screen and (min-width: 800px) {
.left:after { opacity: .4; }
}
@media screen and (min-width: 850px) {
.left:after { opacity: .3; }
}
@media screen and (min-width: 900px) {
.left:after { opacity: .2; }
}
Best seen on the landing page where you can interact with it, but here’s a screenshot of what that looks like at different widths:

One last detail I’ll mention is the header on the /art page, which incorporates one additional panel section that “pops” up at full width. A sharper transform: skewY()
angle makes this work and is the closest I could get to recreating a pop-up page without making my brain melt.

Pull-tabs
Pull-tabs in pop-up books are a really neat technique of layering paper images that move and change soley from movement of the tab along a single axis. To accomplish this with code, I wanted to keep that constraint. The only thing that could trigger an effect was moving the pull-tab to the left/right or up/down.

Of course I hoped I could do this with CSS only, so I tried using resize
as the pull mechanism. It worked in some ways, but with limitations on how much I could change the resize handle, it didn’t quite work.
So next I tried using an <input type="range">
slider. I was delighted to see you could style things pretty well (as seen in this CSS-Tricks article). This was very close to what I wanted to do. In the end though, it didn’t allow for the flexibility I needed. It also had a default behavior when clicking and dragging, where the browser shifts the handle to be exactly center under the cursor. You can see that happening here on CodePen. Not a huge deal, but not the experince I wanted.
At this point I started looking for some drag and drop JavScript help. After trying a few libraries, I landed on David DeSandro’s Draggabilly which was a breeze to implement for me and offered single axis movement and containment. Perfecto.
There are three pull-tab techniques I’m using on the homepage: basic (David Rose and Lynn’s sunglasses), straight dissolve (A Single Div), and angled dissolve (Airport Codes).
Basic pull-tab
The basic pull-tab technique relies on “windowed” layers revealing a moving layer underneath. In the case of David Rose, I’m using a handful of images layered below and on top of the draggable layer (the sweater designs). Here’s a visual of the four layers, with David’s sweater transparent and the surrounding area solid. I changed some colors to hopefully make it more clear.

The solid color surrounding areas obscure the sweater design outside of the transparent window. They also leave an unobstructed area so the tab can be grabbed and dragged. Here’s the movement of the draggable layer with the top layers at lower opacity:

The imagery is positioned absolutely one on top of the other. It’s a mix of inline SVG, <img>
, and CSS background-image
(which I’ll get to later). The draggable layer needs a containing element:
<div class="david-wrapper">
<div class="david-pull draggable">
<img src="david-pull.svg" alt="David’s sweaters" />
</div>
</div>
Then with Draggabilly, I can do this:
var david = new Draggabilly( '.david-pull', {
axis: 'x',
containment: '.david-wrapper'
});
The width of .david-wrapper
limits how far the pull-tab can be pulled, serving as the stopping tab you’d build into a paper pull-tab.
Straight dissolve pull-tab
This technique creates a dissolve transition between one image and another. It works by cutting each image into equal strips and layering them in an alternating stack. It seemed like this could be recreated with z-index
.
Here’s a pen that shows how this works. You can grab the turquoise layers on the right and drag down.
See the Pen Dissolve transition proof of concept by Lynn Fisher (@lynnandtonic) on CodePen.
The markup looks like this, with each <div>
numbered for where it sits in the stack.
<div class="container">
<div class="one">1</div>
<div class="three">3</div>
<div class="five">5</div>
<div class="draggable">
<div class="two">2</div>
<div class="four">4</div>
<div class="six">6</div>
</div>
</div>
Then we position the layers and assign z-index
values that correspond with each layer.
.one {
bottom: 0;
z-index: 1;
}
.three {
top: 33.33%;
z-index: 3;
}
.five {
top: 0;
z-index: 5;
}
.two {
bottom: 0;
z-index: 2;
}
.four {
top: 33.33%;
z-index: 4;
}
.six {
top: 0;
z-index: 6;
}
It’s using the same Draggabilly setup.
var dissolve = new Draggabilly( '.draggable', {
axis: 'y'
});
Except I ran into an issue. Draggabilly by default is moving the draggable element with a CSS transform
and this creates a new z-index
stacking context. So I ended up losing the nice layered effect I had while the element was moving.
A quick fix was to move the element based on its top
value instead of a transform
. Not the best for performance normally, but it’s such a small interaction I figured it was just fine. (Thanks to Jason Rose for helping me with this one.)
dissolve.positionDrag = function() {
this.setLeftTop();
};
In the CodePen example, you can see top layer #6 sitting above the others. On the homepage, I’m using some artwork plus some strategically placed :before
and :after
pseudo-elements to cover up any pieces I don’t want you to see. Here‘s how it looks in the end:

Angled dissolve pull-tab
The angled dissolve works in exactly the same way as the straight dissolve, but the layers of the draggable image are masked with angled transparent PNGs.

Why not use CSS clip-path
? I wanted to! But clip-path
made the image edges aliased, creating a thin line between layers and breaking the illusion. PNGs provided anti-aliasing to give me the smooth edges I wanted.
Here’s how the final effect looks:

Changing portraits
Whew. Still with me? 😅
In lieu of a folding header on the /about page, I illustrated a series of self-portraits dressed as some of my favorite characters. Thanks to Adam Avenir for the idea! As you resize the page smaller, you’ll be able to catch them.
This is accomplished with a big sprite that shifts the background-position
for each media query (similar technique I used for the 2018 Bob’s Burgers animation).
It starts with this base image.
.avatar {
width: 452px;
height: 550px;
position: relative;
background-image: url('avatar-lynn-base.svg');
}
And then layers a sprite on top with an :after
pseudo-element.
.avatar:after {
content: '';
width: 100%;
height: 100%;
position: absolute;
background-repeat: no-repeat;
background-image: url('avatar-lynn-costumes.svg');
background-position: 0 0;
}
A sample from the costume sprite:

Then I can shift the background-position
at each media query to show a different costume.
@media screen and (min-width: 501px) {
.avatar:after {
background-position-x: -452px;
}
}
@media screen and (min-width: 551px) {
.avatar:after {
background-position-x: -904px;
}
}
@media screen and (min-width: 601px) {
.avatar:after {
background-position-x: 0;
background-position-y: -550px;
}
}
... and so on
And it creates this fun effect when you resize the browser:

Aything else?
There’s a couple other easter eggs to discover, but that covers most of it!
Last year I tried out a dark mode theme for prefers-color-scheme
and this year I followed Andy Bell’s user-controlled dark mode tutorial which is just wonderful. It uses CSS custom properties in a way I haven’t before and that was cool to learn.
Remember how I mentioned that some of the pull-tabs use a mix of inline SVG, <img>
, and background-image
? Because of the solid color backgrounds that obscure layers beneath them, I needed to swap those colors for the light and dark theme. Adding a class to SVG paths and changing the fill
color with custom properties made that no big lift at all.

Like with every refresh I learned more about cross-browser CSS behavior, SVG quirks, and different ways to lay things out with grid and flexbox. And I learned more than I ever imagined about pop-up books.
There’s a lot of reasons to do this refresh every year, but one downside is that I don’t code things for long-term maintenance. If it changes in a year, there’s not much pressure to try. But this year I did some intentional cleanup, consolidating, and creating of templates that I should have years ago. Felt really good!
I’ll end this with my usual reminder that previous versions of the site are still viewable in the archive.
Until next year’s refresh. 👋 Thanks for reading!