Recreating Dropbox’s Planes System In CSS

Dropbox went through a significant rebrand in 2017 moving away from light touches of blue against stark white backgrounds to hyper colorized layers of content and imagery. It was controversial to say the least but eventually found its footing to stand apart from competitors who photocopied Dropbox’s original branding. While the plane system isn’t used by Dropbox anymore — you can still find the hard cut effects of planes on dropbox.com/blackops — we can break down how to build the same plane system.

My team was responsible for building a functioning version of planes — prominently used on Dropbox’s homepage — for the inevitable website overhaul paired with the brand refresh. Animating these planes with hard cuts between fixed and relative content was an interesting challenge. We needed a smooth experience for all sorts of devices. Despite this being 2D animation, it was easy to overwhelm a mobile browser’s rendering capabilities if DOM painting wasn’t efficient.

Building the concept of planes sounds easy — just stack content on a z-index and scroll…. right? Kind of. A few distinct hurdles popped up during prototyping.

But, that’s the fun of prototyping! We can validate our concepts or tweak what’s needed to make something work and hopefully move forward with something better than what we started.

Conceptually Dropbox’s plane system functioned like a magical stack of paper. Each piece is a viewport to a fixed view or plane. Each piece can only expose one plane at a time. As the paper slides away, the viewport shifts but the plane remains fixed relative to our field of view. The piece immediately below exposes another plane the same way and relative to what’s in view, we now see two planes. Some elements can be appear to transcend a plane and appear between two pieces of paper. While visually they may be the same, in actual implementation they’re repeated elements unique to each plane.

This maps cleanly to the concept of z-indexing elements on a web page. Making content fixed to the viewport of a web page revealing it only when a new plane is revealed in the stack gets tricky.

Building The Plane System

The plane system in HTML is comprised of several distinct elements that must be nested in a certain order and repeated for effect. This system assumes what’s viewable is the full dimensions of a viewport and what can be scrolled is the full height of a document.

<section class="planes">
  <!-- First plane child -->
  <section class="plane">
    <section class="plane-frame">
      <section class="plane-container gpu-rendered">
        <!-- Normal content goes here -->
      </section>
    </section>
  </section>
  <!-- Just add more planes -->
  <section class="plane">...</section>
  <section class="plane">...</section>
  <section class="plane">...</section>
</section>

Planes

This is where a plane lives as a child in multiples. Sized to the full width and height of a viewport for convenience but could be defined in different dimensions. Just make sure it scrolls.

.planes {
  width: 100vw;
  height: 100vh;
  overflow: scroll;
  position: relative;
}

Plane

.plane is the metaphorical piece of paper in a stack and sized consistently to be uniform with another plane. Sized to a viewport’s height instead of the parent to prevent the parent element (.planes) from growing. Since planes scroll vertically, width is set to the parent and not a viewport.

.plane {
  width: 100%;
  height: 100vh;
  position: relative;
  overflow: hidden;
  z-index: 1;
}

Plane Frame

A container that sets positioning and clipping. A frame needs to be the immediate child of a .plane element due to positioning which creates a pseudo viewport from a clip function. clip can only be used with absolute or fixed positioned elements and restricts what can be shown visibly. Setting clip to use a rectangle that is the full width and height of .plane-frame prevents fixed content in a plane from appearing relative to the viewport when a plane is being scrolled.

.plane-frame {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  clip: rect(auto, auto, auto, auto);
  pointer-events: none;
  z-index: 2;
}

Plane Container

The most important element — pulls content out of the normal flow a document and binds it to a viewport. All content in a plane lives here. While your content may go here, you may need another nested element just for alignment if you’re using grid of flex box. Applying either to a fixed element can cause visual performance degradation and your children’s alignment are being applied to a parent outside the normal flow of the DOM.

.plane-container {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
}

Restoring Pointer Events

You may have noticed an explicit removal of pointer events in .plane-frame. During prototyping I found click events were bleeding through planes. Not surprising given how many different ways normal flows were shifting but distressing nonetheless. This mean click events from plane 1 would overtake plane 4 even when plane 1 was fully out of view. Something, something position fixed. To “fix” that bug, pointer events are removed in .plane-frame and need to be explicitly restored to all immediate children with pointer-events: all. Buttons and anchor links become clickable, text highlightable and video controls interactive.

Performance Optimizations

Using a clip function and altering normal flow of content through an unorthodox positioning flow leads to some performance degradation. There’s an argument to be made for whether this level of nesting serves the content within (it doesn’t) but the plane system establishes a new visual flow of content that doesn’t quite make sense in HTML. It’s a trade off between structured content and visual flair. Let’s lean in to the later.

.gpu-rendered,
.gpu-rendered > * {
  // Forced rendering on GPU
  transform: translateZ(0);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  will-change: transform;
  perspective: 1000;
}

Forcing a plane and its content to be drawn via the GPU is done by transforming elements along the z-axis with the Safari specific hack of setting backface visibility and perspective. 3D elements were never used inside of a plane — videos were the most ambitious animated content — so this hack makes less sense until you realize the performance trade off from not accelerating content painting on a GPU using Planes.

Another trade off is repetitive elements. For the sake making the plane’s hard cut transitions between blocks of color work, global elements cannot be used. That means a traditional fixed navigation needs to be repeated in each plane and fixed to the top of a plane container. Visually this looks really cool. For Accessibility and SEO repeated content add some friction to the user experience. You can make a bad argument that a plane is so self-contained it functions as a stand alone page — if that were the case why not just make it a separate page.

The point to be made with Planes is they focus on clever animation in line with a brand style at the cost of performance and content hierarchy that can hopefully be minimized.

Page Background Color

Planes established a background color that looks like the document being scrolled has a background color. That illusion gets spoiled when you over scroll either end of a document showing the body and html tag background color. Continuing the effect is easy: apply a linear gradient as a background to the immediate parents of .planes that starts with the first color of the first plane — if an element is positioned to the top of the first plane like the nav element below, you’ll use that color instead — and the background color of the last plane as the second color. Add a stop at 50% and 51% to prevent bleeding between colors

html, body { 
  /* Apply gradient from top to bottom */
  background: linear-gradient(
    180deg,
    $firstPlaneBackgroundColor 0%,
    $firstPlaneBackgroundColor 50%,
    $secondPlaneBackgroundColor 51%,
    $secondPlaneBackgroundColor 100%
  );
}

Codepen

Okay, so how does this thing actually look and can I copy this code? It works like this and yes, you may with wild abandon.

Codepen Link

What made building this fun wasn’t the bold direction Dropbox was taking with its brand but prototyping an experiences with Front-End tech that maybe might not work. Always exhilarating when you can entertain the possibility of failure leading to a reduced scope against a non-negotiable deliverable. You can take this demo and use it outright or build something better suited for the web in the year of 2,022. This implementation maps as close as possible to the one shipped in the year of 2,017 and there are likely some optimizations that can be made. Reducing reliance on transforming all children for GPU acceleration and designating viewport visibility with aria labeling based on scroll distance is something I’d revisit today.

Whether this system makes sense from a functionality, design and branding perspective is a question for you to explore. Maybe this was the folly of man realized too late or maybe the cool colors are just too awesome to not stare at. Building systems like these plunges us into murky space that leads to intense ideological debate between designers and front-end engineers. Should something like this exist at the expense of something needed? I’ll leave that as a topic for my next dinner party.