Last week I put together a landing site for a new business. The design was quite straightforward: a single page with various sections represented as full-width horizontal blocks with different background colours, and a fixed positioned logo on the top-left corner.
The only particularity was that the logo should change colours, as the user scrolls, depending on which section it overlaps. When positioned over a section with a dark background, the logo should be white. When overlapping a section with a light background, it should be dark blue.
The first thought that comes to mind is to use two images and swap them as the user scrolls, but that doesn’t work here. If you look closer at the designs above, the second screen shows the logo overlapping a dark/light cross-section. When that happens, the portions of the logo that overlap each section must be coloured accordingly.
Attempt #1: animate position
After searching around, I found this pen. It works by having a master version of the logo, as
position: fixed, and several copies – one for each section – with
position: absolute. As the user scrolls, the position of each copy is moved up or down so it perfectly overlaps the master, creating the illusion that it’s the same element.
During my tests, it worked beautifully in Chrome, but it was quite jittery in Safari.
Attempt #2: SVG clip-path
When attempt #1 failed, I tried to approach it from a different angle. Instead of having one copy of the element per section and animate their positions so they overlap, I created two copies (one dark and one light) of the logo and placed them exactly on top of each other, both with fixed positioning.
Next, I had to find a way of hiding a portion of a logo in such a way that the void created reveals a part of the other one. This sounded like a mask, so
clip-path was a good candidate. When used on HTML elements, its browser support is quite limiting, but my logo was an SVG, in which case the support is pretty much ubiquitous.
Creating the masks
I started by creating an SVG
<clipPath> element for each logo.
Then I created a function that allowed me to manipulate the dimensions and position of the masks in such a way that the exact amount of each logo would be shown. The following function receives two arguments:
amountis the number of pixels to be revealed from the logo that is displayed on top
isDarkis a boolean that defines whether the first section that overlaps the logo has a dark background
Couple of things to note here:
- The examples are using jQuery to more succinctly represent DOM interactions, but it’s absolutely not essential to build this functionality;
maskCacheis an object that stores the last pair of arguments (
isDark) that the function was called with, avoiding touching the DOM for multiple calls with the same values. It’s a performance optimisation that will make more sense shortly.
Mapping the sections
To more easily calculate when the logo is overlapping each section, I mapped each section as an object containing the vertical position at which the section ends, whether the section has a light or dark background and a reference to the DOM node.
Updating the blend on scroll
Finally, we need to update the blend of the masks as the user scrolls. To do that, I first created a function that detects when the logo is overlapping one or two sections, and what the colours of their backgrounds are.
Finally, we just need to attach this function to the
onScroll event and also call it when the page loads.
And it works!
The end result
Here’s how it looks on the same version of Safari that was being problematic before.
You can see it live (and dissect the source) here. ∎