Remember the last time you dealt with a UI-related bug that left you scratching your head for hours? Maybe the issue was happening at random, or occurring under specific circumstances (device, OS, browser, user action), or was just hidden in one of the many front-end technologies that are part of the project?
I was recently reminded of how convoluted UI bugs can be. I recently fixed an interesting bug that was affecting some SVGs in Safari browsers with no obvious pattern or reason. I had searched for any similar issues to get some clue about what was going on, but I found no useful results. Despite the obstacles, I managed to fix it.
I analyzed the problem by using some useful debugging strategies that I’ll also cover in the article. After I submitted the fix, I was reminded of the advice Chris has tweeted out a while back.
Write the article you wish you found when you googled something.
— Chris Coyier (@chriscoyier) October 30, 2017
…and here we are.
Here’s the problem
I found the following bug on a project I’ve been working on. This was on a live site.
I created a CodePen example to demonstrate the issue so you can check it out for yourself. If we open the example in Safari, the buttons might look as expected on load. But if we click on the first two larger buttons, the issue rears its ugly head.
Whenever a browser paint event happens, the SVG in the larger buttons render incorrectly. It simply gets cut off. It might happen randomly on load. It might even happen when the screen is resized. Whatever the situation, it happens!
Here’s how I approached the problem.
First off, let’s consider the environment
It’s always a good idea to go over the details of the project to understand the environment and conditions under which the bug is present.
- This particular project is using React (but is not required for following this article).
- The SVGs are imported as React components and are inlined in HTML by webpack.
- The SVGs have been exported from a design tool and have no syntax errors.
- The SVGs have some CSS applied to them from a stylesheet.
- The affected SVGs are positioned inside a <button> HTML element.
- The issue occurs only in Safari (and was noticed on version 13).
Down the rabbit hole
Let’s take a look at the issue and see if we can make some assumptions about what is going on. Bugs like this one get convoluted, and we won’t immediately know what is going on. We don’t have to be 100% correct in our first try because we’ll go step-by-step and form hypotheses that we can test to narrow down the possible causes.
Forming a hypothesis
At first, this looks like a CSS issue. Some styles might be applied on a hover event that breaks the layout or the overflow property of the SVG graphic. It also looks like the issue is happening at random, whenever Safari renders the page (paint event when resizing the screen, hover, click, etc.).
Let’s start with the simple and most obvious route and assume that CSS is the cause of the issue. We can consider the possibility that there is a bug in the Safari browser that causes SVG to render incorrectly when some specific style applies to the SVG element, like a flex layout, for example.
By doing so, we’ve formed a hypothesis. Our next step is to set up a test that is either going to confirm or contradict the hypothesis. Each test result will produce new facts about the bug and help form further hypotheses.
We’ll use a debugging strategy called problem simplification to try and pinpoint the issue. Cornell University’s CS lecture describes this strategy as “an approach to gradually eliminate portions of the code that are not relevant to the bug.”
By assuming the issue lies within CSS, we can end up pinpointing the issue or eliminating the CSS from the equation, reducing the number of possible causes and the complexity of the problem.
Let’s try and confirm our hypothesis. If we temporarily exclude all non-browser stylesheets, the issue should not occur. I did that in my source code by commenting out the following line of code in my project.
I have created a handy CodePen example to demonstrate these elements without CSS included. In React, we are importing SVG graphics as components, and they are inlined in HTML using webpack.
If we open this pen on Safari and click on the button, we are still getting the issue. It still happens when the page loads, but on CodePen we have to force it by clicking the button. We can conclude that the CSS isn’t the culprit, but we can also see that only the two out of five buttons break under this condition. Let’s keep this in mind and move on to the next hypothesis.
Isolating the issue
Our next hypothesis states that Safari has a bug when rendering SVG inside an HTML
<button> element. Since the issue has occurred on the first two buttons, we’ll isolate the first button and see what happens.
Sarah Drasner explains the importance of isolation and I highly recommend reading her article if you want to learn more about debugging tools and other approaches.
Isolation is possibly the strongest core tenets in all of debugging. Our codebases can be sprawling, with different libraries, frameworks, and they can include many contributors, even people who aren’t working on the project anymore. Isolating the problem helps us slowly whittle away non-essential parts of the problem so that we can singularly focus on a solution.
A “reduced test case” it is also often called.
I moved this button to a separate and empty test route (blank page). I created the following CodePen to demonstrate that state. Even though we’ve concluded that the CSS is not the cause of the issue, we should keep it excluded until we’ve found out the real cause of the bug, to keep the problem simple as possible.
If we open this pen in Safari, we can see that we can no longer reproduce the issue and the SVG graphic displays as expected after clicking the button. We shouldn’t consider this change as an acceptable bug fix, but it gives a good starting point in creating a minimal reproducible example.
A minimal reproducible example
The main difference between the previous two examples is the button combination. After trying out all possible combinations, we can conclude that this issue occurs only when a paint event occurs on a larger SVG graphic that is alongside a smaller SVG graphic on the same page.
We created a minimal reproducible example that allows us to reproduce the bug without any unnecessary elements. With minimal reproducible example, we can study the issue in more detail and accurately pinpoint the part of the code causing it.
I’ve created the following CodePen to demonstrate the minimal reproducible example.
If we open this demo in Safari and click on a button, we can see the issue in action and form a hypothesis that these two SVGs somehow conflict with one another. If we overlay the second SVG graphic over the first, we can see that the size of the cropped circle on the first SVG graphic matches the exact dimensions of the smaller SVG graphic.
Divide and conquer
We’ve narrowed down the issue to the combination of two SVG graphics. Now we’re going to narrow things down to the specific SVG code that’s messing things up. If we only have a basic understanding of SVG code and want to pinpoint the issue, we can use a binary tree search strategy with a divide-and-conquer approach. Cornell University’s CS lecture describes this approach:
For example, starting from a large piece of code, place a check halfway through the code. If the error doesn’t show up at that point, it means the bug occurs in the second half; otherwise, it is in the first half.
In SVG, we can try deleting
<filter> (and also
<defs> since it’s empty anyway) from the first SVG. Let’s first check what
<filter> does. This article by Sara Soueidan explains it best.
Just like linear gradients, masks, patterns, and other graphical effects in SVG, filters have a conveniently-named dedicated element: the
<filter>element is never rendered directly; its only usage is as something that can be referenced using the
filterattribute in SVG, or the
url()function in CSS.
In our SVG,
<filter> applies a slight inset shadow at the bottom of the SVG graphic. After we delete it from the first SVG graphic, we expect the inner shadow to be gone. If the issue persists, we can conclude that something is wrong with the rest of the SVG markup.
I’ve created the following CodePen to showcase this test.
As we can see, the issue persists anyway. The inset bottom shadow is displayed even though we’ve removed the code. Not only that, now the bug appears on every browser. We can conclude that the issue lies within the rest of the SVG code. If we delete the remaining
<g filter="url(#filter0_ii)">, the shadow is fully removed. What is going on?
Let’s take another look at the previously mentioned definition of the
<filter> property and notice the following detail:
<filter>element is never rendered directly; its only usage is as something that can be referenced using the
filterattribute in SVG.
So we can conclude that the filter definition from the second SVG graphic is being applied to the first SVG graphic and causing the error.
Fixing the issue
We now know that issue is related to the
<filter> property. We also know that both SVGs have the filter property since they use it for the inset shadow on the circle shape. Let’s compare the code between the two SVGs and see if we can explain and fix the issue.
I’ve simplified the code for both SVG graphics so we can clearly see what is going on. The following snippet shows the code for the first SVG.
<svg width="46" height="46" viewBox="0 0 46 46"> <g filter="url(#filter0_ii)"> <!-- ... --> </g> <!-- ... --> <defs> <filter id="filter0_ii" x="0" y="0" width="46" height="46"> <!-- ... --> </filter> </defs> </svg>
And the following snippet shows the code for the second SVG graphic.
<svg width="28" height="28" viewBox="0 0 28 28"> <g filter="url(#filter0_ii)"> <!-- ... --> </g> <!-- ... --> <defs> <filter id="filter0_ii" x="0" y="0" width="28" height="28"> <!-- ... --> </filter> </defs> </svg>
We can notice that the generated SVGs use the same
id=filter0_ii. Safari applied the filter definition it read last (which, in our case, is the second SVG markup) and caused the first SVG to become cropped to the size of the second filter (from 46px to 28px). The id property should have a unique value in DOM. By having two or more
id properties on a page, browsers cannot understand which reference to apply, and the
filter property redefines on each paint event, dependent on the racing condition that causes the issue to appear randomly.
Let’s try assigning unique
id attribute values to each SVG graphic and see if that fixes the issue.
If we open the CodePen example in Safari and click the button, we can see that we fixed the issue by assigning a unique ID to
<filter> property in each SVG graphic file. If we think about the fact that we have non-unique value for an attribute like id, it means that this issue should be present on all browsers. For some reason, other browsers (including Chrome and Firefox) seem to handle this edge-case without any bugs, although this might be just a coincidence.
That was quite a ride! We started barely knowing anything about an issue that seemingly occurred at random, to fully understanding and fixing it. Debugging UI and understanding visual bugs can be difficult if the cause of the issue is unclear or convoluted. Luckily, some useful debugging strategies can help us out.
First, we simplified the problem by forming hypotheses which helped us eliminate the components that were unrelated to the issue (style, markup, dynamic events, etc.). After that, we isolated the markup and found the minimal reproducible example which allowed us to focus on a single chunk of code. Finally, we pinpointed the issue with a divide-and-conquer strategy, and fixed it.
Thank you for taking the time to read this article. Before I go, I’d like to leave you with one final debugging strategy that is also featured in Cornell University’s CS lecture.
Remember to take a break, relax and clear your mind between debugging attempts.
If too much time is spent on a bug, the programmer becomes tired and debugging may become counterproductive. Take a break, clear your mind; after some rest, try to think about the problem from a different perspective.