Redesigning Heap Without a Feature Branch
Heap has a new look! Over the past three months we completed an overhaul of our web app’s design and made it more consistent, modern, and delightful. Dave, our VP of Design, gave a great write-up of our approach to creating this new design, and I’d like to share a few tricks our engineering team used to execute the project smoothly.
We continuously shipped the updated design to production piece by piece over a three-month period prior to release. During development, small updates to the UI were written, reviewed, tested, and deployed regularly while other engineers on the team built and released exciting new features independently.
It took some careful planning and scoping to enable this workflow, but the result was worth it: on the day of the release, we flipped a switch and everyone saw the updated UI when they loaded Heap. There were no major feature branches to re-integrate with master
, no herculean QA efforts, no surprises for documentation or product demos, and no hiccups delivering the new look to everyone using the product.
Before…
After!
The Challenge
To be totally honest, when we first started planning this project, I felt a little apprehensive. Like many other highly visible changes, UI overhauls are notorious for ballooning in scope. They attract the “while you’re at the store” kind of scope creep: “While you’re working on the toolbar, do you mind replacing its dropdown menu with this tabbed menu instead?” Each extra thing you pick up “at the store” makes it a little harder to cram everything into your pack before the trip home. I was concerned that this project could suffer a similar fate.
Pulling this project off meant we had to be very honest with ourselves about what motivated us to take the project on in the first place and to scope the project accordingly.
Our first motivation was that Heap’s look and feel was dated, and we wanted to give it some love. Second, our existing visual design language was not particularly systematic, and we were finding it difficult to introduce new features in a way that was visually consistent with our existing features. We wanted to consolidate several patterns that occurred throughout the app and create more opportunities for future patterns to emerge. Third, we wanted to replace our entire in-app icon set with a collection of icons that better reflected the concepts presented to our users. Heap can be a tricky product to wrap your mind around, and our existing icon set (essentially a collage of icons engineers scrapped together one at a time while building features) wasn’t communicating in-app concepts clearly.
On the flip side, going into the project we knew we weren’t motivated to change the overall information architecture of the app. In other words, if a feature existed on a particular page, we didn’t want to move it to a different page. We also didn’t want to fundamentally change how things were laid out inside the UI. If a button was in a certain spot in relation to other elements in the UI, it was probably going to stay there. People have powerful workflows built out within the Heap app, and we didn’t want to disrupt their work as a result of the project. We considered the task of improving these workflows as a separate design challenge for a separate project. The work we intended to do in this project would open up possibilities for UX improvements that we couldn’t consider given our current system.
As a result of these conversations, we decided the scope of the project would revolve primarily around changes to stylesheets and not so much around markup or behavior.
The Approach
A guiding principle of the engineering work we do at Heap is to validate changes in production early and often. In our database infrastructure, this manifests in a system we call shadow prod. In our user-facing functionality, this shows up in a more familiar concept: feature flagging.
Feature flagging is a method of gating functionality in an application behind a conditional check, often determined at runtime. This usually results in logic of the form, “If this new feature is enabled for this user, show a link to it in the sidebar.” Feature flagging separates the act of integrating and deploying changes from the act of releasing features, and it allows a Heap engineer to integrate their changes with everyone else’s quickly. It also has the added benefit of reducing the QA burden on a project, as it’s much easier to catch buggy behavior incrementally than through large QA passes done shortly before release.
We considered the visual refresh no different from any other engineering effort at Heap. Thus it was important to make these changes incrementally behind a feature flag to reap all the benefits we have come to expect in our engineering process. This was tricky!
Making the Problem Tractable
With the project primarily scoped down to stylesheet changes, we started exploring some ways of making changes to our CSS behind a feature flag. During project planning we identified a few tactics to introduce these changes to production without affecting users who did not have the feature flag enabled.
We dropped in a piece of logic to apply a class name to the top-level html
element of the app to leverage in CSS when the flag was enabled. The class name we chose was .ui-apply-visual-refresh
, primarily because its intent was clear, and it passed the Grep Test. This meant that we could add redesign-only CSS anywhere in the product and flag it on or off based on one branching point, but it didn’t complete the picture of how we could reliably make changes to the existing styles.
Branching CSS Selectors
The first kind of change we explored was to scope changes to the top-level .ui-apply-visual-refresh
class and scope “old” styles to not that class (e.g., html:not(.ui-apply-visual-refresh)
). We use LESS, a CSS preprocessor that supports nested rules, so this kind of change is rather easy:
Before
.some-selector {
width: 100px;
}
After
html:not(.ui-apply-visual-refresh) {
display: inline-block;
border: 1px solid blue;
}html.ui-apply-visual-refresh {
display: inline-block;
border: 1px solid red;
}
Essentially we’re separating the “incoming” styles from the “outgoing” styles. With the feature flag enabled and the top-level class present, new styles will apply to the document. Without the flag, the original styles will apply.
However, things aren’t as simple as they may seem. This small act of nesting the rule changes the specificity of the selector. Specificity is used as a tie-breaker when multiple conflicting rules might apply to the same element, so it may have unintended consequences in places where .some-selector
is used. Perhaps the width
was overridden downstream to make the element fill the width of its container or perhaps the border
was removed somewhere else. This change would have introduced a visual regression in those places because the winning rule in a tie would have changed.
In some places in our stylesheets, we had moments where these delicate cascading effects had really calcified, and the work of examining them and chipping away at their complexities didn’t look like a viable option given our time constraints and the extent of the project. In these cases, we needed an alternative that was easy to reason about.
Extracting to CSS Custom Properties
Our solution to this specificity trickiness turned out to be a relatively simple mechanical operation: extract a declaration block’s property values to a custom property and then conditionally set those values based on the presence of the feature flag.
Custom properties (often referred to as CSS variables) are a relatively new feature in CSS, but they’re essentially a way of reusing values inside CSS. The value of a custom property is scoped to the selector in which it’s defined, and its value cascades. If a custom property is defined in several selectors, its most specific selector wins the tie, so its value is dependent on which selectors apply. In other words, a custom property defined within the html.ui-apply-visual-refresh
scope can override the value of the same property only defined within the html
scope.
This meant we had a mechanism we could use to preserve old styling by default and incrementally introduce new styling without untangling complicated cascading schemes. Mechanically, it looked like this:
Before
.some-selector {
width: 100px;
}
After
html {
}html.ui-apply-visual-refresh {
}.some-selector {
width: 100px;
}
By extracting the value of the border
declaration to a custom property, we get the same effect as before, but the specificity of .some-selector
doesn’t change. This approach dodges the complexity incurred by the cascading styles in the previous approach and allows us to make targeted changes to the way things are styled without changing behavior when the feature flag is disabled.
There is one caveat to this approach, and that is browser support. Custom properties are supported in all major browsers except IE11 and earlier. A polyfill exists with the constraint that variables can only be defined at the :root
level. They are otherwise ignored. This constraint with the polyfill didn’t affect our particular implementation as we could use the :root
scope for the original values and make our changes within the :root.ui-apply-visual-refresh
scope. This would preserve the original styles for IE11 users, and we could do our work using a more modern browser. The constraint did imply that a small code change was required to test changes in IE11 during development and during release.
The combination of this extraction to custom properties and the branching technique yielded a simple toolkit that enabled us to move forward on the project. We could make changes incrementally and merge them into master
without changing what users saw when they didn’t have the feature flag enabled.
We also considered using the feature flag to switch which stylesheets were included on the page. For example, there might be stylesheet.original.css
and stylesheet.refreshed.css
, and the feature flag would determine which file is injected on the page. The rules inside the refreshed stylesheet could have identical specificity, and there would be no reason to change the original code. For a while this seemed like a viable option, but as we dug into it, it became clear that the code duplication that this approach could complicate the feature work that other engineers were doing. In retrospect, I think this could have been a viable option in a few places that weren’t frequently touched.
Replacing Every Icon
Without a well-established system for icon design, we had collected a mix of icons over the years with different aspect ratios and approaches to coloring and fill. The inconsistencies led to an organic trend of engineers making on-the-ground adjustments to place the icons neatly inside their surrounding context. There was a lot of shimming code to do things like shift an icon’s position horizontally or vertically, change its width, adjust its opacity, or set its fill. We wanted to get rid of this shimming code, and have a clean system moving forward.
Auditing the code for every case of inconsistency was a daunting task to do by hand. To make this work easier, we crafted a very ugly placeholder icon that reflected our guiding constraints for the incoming icon set: a 20×20 unit square bounding box with a soft margin of 1 unit reserved for “wiggle room.” The placeholder also featured some traits strategic for visually identifying different cases where ad-hoc styling was applied. It had multiple fill colors inside and a red stroke for indicating the soft margin. Here’s an abridged look at the placeholder’s markup:
<svg width="20" height="20" viewBox="0 0 20 20">
stroke="#f00"
fill-rule="nonzero"
/>
</svg>
Each trait of the placeholder was useful for visually identifying a particular moment of in-place styling. When an icon’s opacity was changed, the entire placeholder would fade out. If styles existed overriding the icon’s fill
, the “?” glyph would match the color of the square and appear invisible. If the icon was scaled somewhere, the element would no longer be 20×20 pixels in dimension. Making each of these kinds of tweaks easy to visually identify reduced the friction in pinpointing the small organic inconsistencies introduced across years of work. It also allowed us to work while the new icon set was being made. As our new icon set took shape, we trickled the completed icons into the UI one by one as they were available.
Releasing the Changes
A key point of this workflow is the flexibility to enable or disable the changes on-the-fly in production. This meant that preparing the project for release was very easy.
About a month ahead of our intended release date, we enabled the UI refresh internally for all Heap employees on our production account. This allowed our marketing and documentation teams to start staging updated screenshots ahead of the release. It also allowed us to get early feedback on the changes and get some passive QA from internal use. More eyeballs looking at the new UI meant more opportunities to catch anything that might have slipped through the cracks. With a month left in the project, plenty of work was still in progress, so we also used this feedback as signal for prioritizing that work.
About three weeks ahead of release, we rolled the visual refresh out to a cohort of customers to beta the new UI. We received some valuable feedback in this period, and made some changes in response.
On the day of release, we enabled the feature flag for everyone else and sent a product update email to all of our customers. That’s it! All the hard work of QAing and releasing the visual refresh happened during the project’s development cycle.
Final Thoughts
Through clever application of some CSS tricks and an uncompromising conviction to our engineering practice, we turned what could have been a risky project into a tractable and pleasant three-month endeavor. We touched nearly everything a user might see in the UI, but we made the changes incrementally without blocking feature work. If you’re about to embark on a project with similar scope or goals, I strongly encourage seeking ways to introduce the changes incrementally and deploy them often. A little bit of planning goes a long way!
Finally, if this kind of work sounds fun or interesting to you, you bet we’re hiring!