Fun CSS-only scrolling effects for Matterday

15 June 2022

Last week my team launched a li’l project called Matterday. Turns out switching to Netlify saves development teams one day a week per developer. That’s a lot of time! And we hope folks can spend that time on things that matter to them.

The narrative portion of the site encourages folks to imagine what they could do with that time — whether it be small changes to daily routines or taking big swings. As you know, a narrative-heavy website is just begging for some fun scrolling effects. And since I’m me, I wanted to see what I could do with just CSS.

I started with a concept I used for a previous version of andyet.com that utilized fixed positioning and z-index to create layered scrolling artwork.

This time around I wanted to explore the idea of layers obscuring and revealing things to create different illusions and to experiment with scroll speeds.

Creating HTML and CSS layers

With CSS alone you can’t inform the page where you are with things like scroll position and triggers (not yet anyway). Elements can’t be told how to change; they are effectively in one state forever. You could technically have elements animating on a loop, but you wouldn’t be able to control when in the sequence someone might have it visible in their viewport. So creative layering is the path I took to try and bring some additional interest to static visuals.

The structure of the page can be simplified down to three main layers: .background, .overlay, and .foreground layers. The overlay layer includes the rounded rectangle “viewport” on the left. The background layer includes the patterned backgrounds that show through the viewport layer plus the copy on the right. And finally the foreground includes the pieces of artwork that scroll on top of both previous layers.

an annotation labeling the background, overlay, and foreground layers
an isomorphic diagram of the layers

The background layer is split into two halves, with the background artwork on the left and copy on the right. The markup looks like this:

<section class="the-office">
  <div class="background">
    <div class="background-container">
      <div class="background-artwork"></div>
    </div>
    <div class="content">
      <h3>Watch all 9 seasons of <em>The Office</em> four times through.</h3>
    </div>
  </div>
</section>

And is rendered like this (shown here with overlay and foreground hidden):

the site divided in two halves, image on left and text on right

I keep adding sections and they create a long page you can scroll through like you would expect:

The overlay is made of two nested <div>s. The .overlay container has a position: sticky so it stays fixed to the top even as its container scrolls. It uses linear-gradient (shown in the screenshot in red) to obscure the areas above and below the “viewport” and the <div> inside is transparent (to show the background layer beneath). A border-radius and box-shadow provide the viewport’s rounded rectangle shape.

diagram showing the placement of the overlay
<div class="overlay">
  <div></div>
</div>
.overlay {
  --body-bg: #0f6a80;
  --bg-position: calc(50% - 30vh);
  --overlay-w: 35vw;
  --overlay-h: 60vh;
  width: 50%;
  height: 100vh;
  position: sticky;
  top: 0;
  left: 0;
  display: grid;
  place-content: center;
  z-index: 1;
  background-image: linear-gradient(to bottom, var(--body-bg) var(--bg-position),
                                               transparent    var(--bg-position)),
                    linear-gradient(   to top, var(--body-bg) var(--bg-position),
                                               transparent    var(--bg-position));
}
.overlay div {
  width:  var(--overlay-w);
  height: var(--overlay-h);
  border-radius: 1em;
  box-shadow: 0 0 0 .5em var(--body-bg);
}

We’ve created a little “window” for the backgrounds to pass beneath to the left of the content. The fixed background gradient and dot pattern, despite being only on the right side of the page, help the two sides feel cohesive.

So here’s how the background and content sections scroll before we add in the foreground layers.

Parallax visuals

Now let’s look at the foreground pieces. The majority of the sections are using a CSS parallax technique using a combination of position, perspective, and 3D transform. This is a pretty tried and true way to have different elements on a page scroll at different speeds. This article by Keith Clark and the accompanying demo are super great for dissecting how this works.

I don’t intend to duplicate Keith’s tutorial here, so at a high level, what the CSS is doing is moving layers forward and backward in space (with translateZ). This creates visual parallax, where things farther in the distance move slower than those up close to you (like looking at scenery go by outside a car window).

isomorphic diagram showing scale and depth for parallax layers, with layer-3 appearing closer to the viewer and moving faster

The parallax structure setup looks something like this (again, I encourage you to read Keith’s awesome tutorial):

<div class="parallax">
  <section class="parallax-group">
    <div class="parallax-layer">
      <img />
    </div>
  </section>
</div>
.parallax {
  height: 100vh;
  perspective: 300px;
}
.parallax-group {
  height: 100vh;
  transform-style: preserve-3d;
}
.parallax-layer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

After each background/content section, a foreground section is added with class .pieces. That full markup ends up looking something like this:

<section class="parallax-group the-office">
  <div class="parallax-layer background">
    <div class="background-container">
      <div class="background-artwork"></div>
    </div>
    <div class="content">
      <h3>Watch all 9 seasons of <em>The Office</em> four times through.</h3>
    </div>
  </div>
</section>
<section class="parallax-group pieces the-office-pieces">
  <div class="parallax-layer foreground layer-1">
    <img src="/images/the-office-jello.svg" width="237" height="148" alt="a stapler stuck in a Jello mold" class="jello" />
  </div>
  <div class="parallax-layer foreground layer-2">
    <img src="/images/paper-airplane.svg" width="173" height="215" alt="paper airplane" class="airplane" />
  </div>
  <div class="parallax-layer foreground layer-3">
    <img src="/images/the-office-mug.svg" width="160" height="145" alt="World’s Best Boss mug" class="mug" />
  </div>
  <div class="parallax-layer foreground layer-2">
    <img src="/images/the-office-beet.svg" width="200" height="274" alt="a beet" class="beet" />
  </div>
</section>
<section class="parallax-group space-mountain">
  ...
</section>

The demo (and lots of examples you may see) uses the parallax effect on full-width sections which creates a lot of movement. The Matterday site applies the parallax to individual artwork pieces to give them a floating feeling relative to each other. Each of the images is given a “depth” by setting a different translateZ value. To keep things manageable, I have three values set and assigned to classes that I can apply to individual elements.

.parallax-layer.layer-1 {
  transform: translateZ(100px) scale(.71);
}
.parallax-layer.layer-2 {
  transform: translateZ(175px) scale(.5);
}
.parallax-layer.layer-3 {
  transform: translateZ(200px) scale(.5);
}

To place the artwork next to the content section it supports (the one right above it in the page flow), I add a negative margin to those sections.

.parallax-group.pieces {
  margin-top: -100vh;
}

And then each individual piece is tweaked and positioned within its parent.

.the-office-pieces .mug {
  width: 20vw;
  transform: rotate(2deg);
  margin-left: 25vw;
  bottom: -10%;
}

When layering elements like this, you want to be careful you aren’t making any content inaccessible. The copy on the page should still be selectable with a mouse. You could add pointer-events: none to the artwork so you can still access the layer below it, but I opted to make the foreground sections half the width of the viewport so they aren’t overlapping the content at all.

.parallax-group.pieces {
  width: 50%;
}
screenshot showing imagery doesn’t overlap text

So in the end, the parallax creates an effect like this:

It creates some nice, subtle movement without feeling like it’s overwhelming or intense scrolljacking. Next I’ll talk through a few of the specific effects (and some tradeoffs).

Re-stacking CSS layers for the “focus” effect

For the section of the narrative “You could sharpen your focus,” I wanted to do a looking glass kind of effect where an element could scroll over a blurry image and focus it. I thought maybe I could lean on CSS filter here or mix-blend-mode or some combination. But because the background and foreground are split up, I had to think about it a bit differently.

The background is set up as normal, but with a handy filter: blur() on it. I also had to scale() it up a wee bit to avoid the feathered edges that CSS blur can cause.

.focus .background-artwork .focus-shapes {
  background-image: url('/images/shapes.svg');
  background-size: 130% auto;
  background-position: center center;
  filter: blur(.6em);
  transform: scale(1.3);
}

The circle effect is made with two foreground layers. The first contains the same background image but not blurred and the second has the teal outline. The effect I wanted was for the circle to pass over the background and “focus” only while it’s over the viewport. To achieve this, I applied a lower z-index to the first circle so it sits below the overlay but still above the background layer. Here is the code (simplified a bit):

<section class="parallax-group pieces focus-pieces">
  <div class="parallax-layer foreground layer-1">
    ...
  </div>
</section>
.parallax-group.focus-pieces {
  z-index: 0;
}
/* Remember that the .layer-1 child will have this transform */
.parallax-layer.foreground.layer-1 {
  transform: translateZ(100px) scale(.71);
}

Both foreground layers are on the same parallax depth (that translateZ) so they move at the same rate and create the illusion of interacting with the background layer.

One small hiccup here. Safari doesn’t like this z-index trick. Because of the translateZ on the foreground layers, it won’t allow for the container to sit below the overlay. A bit of a bummer (but also I get why it behaves that way). Resetting the translateZ does the trick, but then you lose the parallax that makes it feel a tiny bit nicer. So I opted to reset the translateZ only for Safari in this case (you can see the code here, not the nicest CSS I know sorry!).

Using the CSS parallax speed variations for the receipt printing effect

The “Time really adds up” section has a similar thing happening. To make it look like the calculator is printing the receipt, it requires the receipt to “grow” out from behind the calculator’s body (farther away) but to move faster (closer). So the receipt gets .layer-2 and the calculator front gets .layer-1, but will they layer like we want?

<!-- receipt -->
<section class="parallax-group pieces">
  <div class="parallax-layer layer-2">
    <div class="addition-receipt">
      <span>60 min</span>
      <span>+ 60 min</span>
      <span class="total">120 min</span>
      ...
    </div>
  </div>
</section>
<!-- calculator front -->
<section class="parallax-group pieces">
  <div class="parallax-layer layer-1">
    <img src="/images/addition-front.svg" width="330" height="374" alt="addition machine" />
  </div>
</section>

Yes! Mostly. Luckily because the .layer-2 receipt comes first in the markup before the .layer-1, the receipt sits behind the calculator front (except for in Safari which again requires some specific fixes).

The gradient trails below the calculator serve the purpose of obscuring the long receipt when needed. Because the artwork is using vw units to scale depending on the width of the browser, if the window is taller than it is wide, the receipt would sometimes show.

bottom of receipt can be seen below calculator and a label says “Noooo”

There’s probably better ways to deal with this, but just gotta ship sometimes. Turns out responsive design can be tricky! So many conditions to consider. 😅

Using cutout images for the “Someday” list effect

Finally, a pretty simple but fun detail from the “You could take items off the backburner” section. It features a Trello-like list of items with pill labels that appear empty at first. As you scroll, each pill turns teal with a “Ready” label.

This is achieved with some artwork prep. The pill shapes are cut out from the list “container” so it reveals whatever is behind it. The “Ready” text is the same color as the dark background so you can’t see it until the artwork is above the teal. No CSS needed, the image is doing the lifting here.

the “Ready” label is only visible with a light background color

But why use CSS instead of JavaScript?

Why do it like this? I love trying new things with CSS. It’s powerful and there are so many cool techniques to experiment with. For most animated scroll experiences, Green Sock really is king and we recently used the heck out of it for the Netlify homepage (shoutout to Justin, Ryan, and Sam for amazing work there). So I figured, why not take a stab at something different? It’s just really fun.

More related to Matterday

Thanks for reading! 👋

···

This was originally published on netlify.com/blog.
Back to Thoughts