Launching New Site and Tracing a Styling Bug
January 30, 2021
First off, welcome to the new site! It has been quite a journey over the last year working towards delivering the site. We hope you stick around as we have heaps planned for Triple E in 2021.
Styling Issues in the Modern Web
The ability to use code from third-parties through easy to use package management tools such as npm
is one piece that makes modern web development so powerful. Code sharing in this fashion also has a compounding effect where packages you install also rely on third-party packages.
While this sharing is great for building functionality rapidly, it can have unexpected side-effects on the visual or behavioural aspects of your site.
We experienced this kind of side-effect with a package that modified the style of a DOM element which in turn impacted the positioning of another element on the page.
First, Some Context
To understand the issue we faced, we need to provide some basic context first. When reading an article, you may have noticed that if you scroll up an arrow icon will appear in the bottom corner. When you click on the arrow the page will scroll up to the top.
How the arrow should display on the page
Recently, the arrow icon stopped appearing at the bottom of the screen. Instead, it showed up at the bottom of the page and would not follow the scroll.
How the arrow was displaying (not following scroll)
What’s the Deal?
Since the feature was functioning at one point we know that the issue was introduced by a change in the codebase. An effective approach to finding the root cause is to perform a binary search and determine which commit introduced the bug. In fact, git
has a built-in feature to assist in performing this type of search called bisect.
Interestingly, we discovered early on that the issue was occurring inconsistently. We found that the arrow had the scroll issue after navigating between pages, but if the page was refreshed or accessed using a direct link the arrow would scroll correctly. Rather than go down the git bisect
path, we can investigate this inconsistency further.
Based on this behaviour we can infer that the cause is likely due to the transition between pages. To find the root cause, we can compare the DOM generated after navigating with the DOM generated after accessing the article directly.
Diffing The DOMs
Since we know the bug is transient, we can assume that there is a difference in state between the navigated page and the directly-accessed page. Finding this difference is as simple as saving the two DOM states and comparing them.
Copying DOM structure from dev tools
Once saved we can use vscode to compare them
From the diff above, we can see that there are a few differences between the two DOMs. The majority of differences are from the reordering of HTML elements that have no impact on the semantics of the page. However, we do notice one difference in a div
’s inline styling that could impact the behaviour.
<div
class="tl-wrapper tl-wrapper--mount tl-wrapper-status--entered"
- style="z-index: 1; opacity: 1; transform: translate(0px, 0px);"
+ style="z-index:1;opacity:1"
>
The div
has a transform
property in its style which is not present in the DOM when directly accessing the page. We can test our hypothesis that this inline style is impacting the arrow by modifying the style in dev tools and testing how it behaves.
The arrow is working again after removing the transform
property!
Root Cause Analysis
Now that we know the cause of the bug we need to dig deeper to find why the transform
property exists on the element in the first place. We already have two clues on where we can start looking. The first clue is that the transform
property is present only after navigating between pages. The other clue is in the name of the div
class; specifically tl-wrapper
. Searching for tl-wrapper
reveals why the first hint should have been an obvious one.
The div is inserted as part of the gatsby-plugin-transition-link
module
As it turns out, we use gatsby-plugin-transition-link
to animate the transition between pages on the site. The reason why this bug only started occurring recently is because we made a change to the transition animation. To make matters worse, the change was made to circumvent a (different) visual bug.
The plugin itself has two main components; a component where animations can be added called TransitionLink
, and, a component with a few pre-built animations to provide an out-of-the-box experience called AniLink
.
To avoid building our own animations, we used the AniLink
component with the built-in fade
animation. We switched from the fade
to the paintDrip
animation to get around the visual bug we mentioned earlier.
If we take a look at the PaintDrip
component which AniLink
wraps via a prop, we find the return
statement specifying the structure of the component.
return (
<>
<TransitionLink
exit={{
length: aniLength,
trigger: ({ exit, e, node }) =>
this.createRipple(exit, e, props.hex, props.color, node),
}}
entry={{
delay: aniDelay,
length: aniLength,
trigger: ({ entry, node }) => this.slideIn(entry, node, direction),
}}
{...props}
>
{props.children}
</TransitionLink>
</>
)
The exit
prop on the TransitionLink
component sets the animation function which will be called when the navigation is triggered and we are “exiting” the page. For the PaintDrip
component the exit function is called createRipple
. Similarly, the entry
prop sets the animation function (slideIn
) that will be called when entering the new page.
slideIn = ({ length }, node, from) => {
gsap.from(node, length, {
...this.getDirection(from),
ease: 'power1.easeOut',
})
}
We can see that the slideIn
function calls gsap.from
which is a function inside the GSAP animation library. Effectively, slideIn
uses gsap
to slide the tl-wrapper
element onto the screen when entering the page. This is why the tl-wrapper
element exists; it wraps the page so that it can be animated, taking all the other elements on the page with it.
Dev tools show the transform property changing state while navigating
How We Should Fix it Versus How We Will
Based on all of the information we have gathered so far on the bug, it seems it may be a deep-lying issue in the gsap
library. Since gsap
is open source we may be able to fix the issue and open a pull-request to get the fixed merged upstream. Unfortunately, gsap’s GitHub page shows that the project is not very contributor-friendly 😞. Even if it was, there is a significant overhead in learning the ins and outs of an unfamiliar project before being able to fix the issue.
Alternatively, we could re-implement the PaintDrip
component using a different underlying animation library. This approach still has a significant overhead as it involves learning the API of a new animation library and does not necessarily mitigate the risk of a similar issue occurring.
There is a node module called patch-package
which allows the user to modify an installed module and commit the diff (“patch”) to the project. The package runs a postinstall script to apply the patch on machines after installing the modules for the first time.
In our case we could use patch-package
to modify the PaintDrip
component and remove the slideIn
entry transition. However, there is an even simpler approach which would have the exact same effect.
.tl-wrapper {
transform: none !important;
}
This small css snippet demonstrates how we can nullify the transform property on the tl-wrapper
class. By adding !important
we can override the transform
property and set it to none
.
Importing the style in gatsby-browser.js
exposes it
By overriding it the transform property no longer has an effect on the layout.
The inline style no longer impacts the arrow display
The Best Way Is Usually Not the Quickest
In an ideal scenario, we would have spent the time to fix the issue in the GSAP animation library. However, given the circumstances surrounding the ability to contribute to the library, and the complexity of animation libraries in general, that wasn’t possible.
More often than not software development does not work in this ideal manner. Typically you need to deliver code in a timely manner and spending time working on a third-party library may not be an effective use of development. Unfortunately the “hacky” approach may be the only viable option given the existing circumstances.
Either way, we managed to fix our mispositioned arrow and learned a bit about side effects with third-party libraries along the way.