How to "Elevate" a User Experience

We dramatically redesigned our product to improve usability — without having to reimplement everything from scratch.

Byron Yang
he/him
Software Engineer

Introduction

At Lattice, our mission is to make work meaningful by building software that drives effective employee relationships, career growth, and a people-conscious culture.

Since our start in 2015, Lattice has gone through two major navigation and UI overhauls. Each navigation change was done to improve the end-user experience, and also to modernize the way users access our growing set of tools.

Several years ago, we redesigned Lattice from a product based navigation to focusing the app around user personas (below), and that approach has served us well. We also received a lot of valuable feedback from our customers about their user experience around that pattern.

Lattice's original user homepage
Lattice's original user homepage

Since the first redesign, we've added Engagement Surveys and Grow (career pathing tools) to our major product offerings, and made a ton of other smaller improvements and updates. We've also grown as an organization, and continued to refine our ideas of what it means to make great HR software. Late last year, the time had come for us to reconsider our user experience from a holistic standpoint. This initiative became known at Lattice as "Project Elevate", or eventually just "Elevate".

Preparation & Requirements

Before getting started, we considered our high-level requirements, with the mindset that we'd aim for a practical solution that allowed us to move fast and iterate quickly. They were as follows:

  • The existing/old Lattice UI should not be affected as we work on Elevate
  • The team should be able to easily turn Elevate on/off for a specific company
  • Engineers on other teams should be able to continue working on other projects, and incrementally adopt the new UI in their dev environments
  • The backend GraphQL API should not need major changes
  • Full screen pages, like Review Cycles and Engagement surveys, should not change at all
Our original full-screen dialog for writing performance reviews

  • Some existing parts of the UI, like the Feedback submission modal and 1-1s content, weren't redesigned. They should not change, and will get reused in the new UI
Editing a 1:1 agenda in the Elevate layout
Lattice's original peer feedback form in the new layout

Feature Flags

Often when we build a new feature, we want to release it initially to a subset of companies, and we do that via feature flags.

Entire third-party services exist to manage feature flags, but, at least for now, we simply store a map of companies to feature flags in our database. We then use that data throughout our codebase on both the backend and frontend to determine business logic and what the user sees in the Lattice UI.

Feature flags allow us to change the behavior of our web application immediately for a specific company, without shipping code. Additionally, we use an internal app that allows other teams, like Customer Care and Product, to easily toggle them at any time.

We decided to use our familiar feature flagging mechanism to allow us to build out the new UI and test it before releasing it to all customers. More on this decision later.

Implementation

We like to think of Project Elevate as rebuilding the "Lattice shell." 90% or more of the work was done on the frontend, a handful of pages weren't changing, and we made minimal changes to our backend GraphQL API. We decided to leverage feature flags to expose new UI on the frontend.

/ and /2 routes

At this point, we aimed for a solution that would both allow for other engineers to continue building the existing app as normal and create a clear separation between the new and old UI. We also prepared for the case that some companies might want to temporarily continue using the old Lattice UI, so we eventually settled on making a less-noticeable change to the URL by prefixing routes with new UI with /2.

Companies with Elevate enabled would get redirected to "/2 routes" and companies with it off would get redirected to (existing) "/ routes." One benefit of this pattern was that it would also take care of routes that might exist in both worlds, like /users/<user_id>, and make it relatively simple to redirect to one or the other by adding or removing /2. This would also help us redirect users to the correct UI when clicking links in previously sent email and Slack notifications.

With Elevate also came a set of new routes to be added, as one of the primary goals of the UI was to make each of our products a “first class” citizen. This meant adding new top level routes, like /reviews, /goals, and /grow. Additionally, a subset of Lattice pages that we called "full page routes," like the Performance Review UI and the Engagement survey UI, were not changing at all, so we left those routes unchanged and didn't prefix them with /2.

Therefore, the routes in Elevate did not map 1:1 to the old UI, and we weren't able to check solely feature flag states to determine intended redirect routes, which added complexity. As we started testing internally, a common bug was an existing notification link leading to a "404 Not Found" page in Lattice.

We'll take a deeper look into how we handled routing between / and /2, depending on feature flag state and the particular page being visited, and some of the challenges.

In our frontend code, we have a file named src/utils/routes.js that exports every route, as a function, that exists in the Lattice app. This allows us to make changes to routing without having to update every route in our frontend code. Here's a snippet:


And, for example, when we redirect to the Home page using React Router, we write the following code.


In our frontend "root" React component, we rendered a new component that included logic to redirect users to the correct UI upon landing in Lattice.


Here's the helper functions that were imported in the code above, from src/utils/elevate.js.


This pattern generally served us well, but it was also an area that felt somewhat brittle and opened us up to mistakes. We ended up having to do a lot of manual work to ensure redirects all worked correctly. The biggest challenges with this pattern were:

  • Forgetting to account for any specific route. We'd attempt to redirect to / or /2 but the corresponding route didn't exist sometimes. We lacked a solid way to test all of these redirects and prevent regressions as we worked through the project.
  • Furthermore, we simultaneously migrated our frontend to use Next.js, which came with a brand new, file-based routing paradigm. This increased the chance for any single route to not get accounted for, as we needed to create a new file for every one of them. Much QA was done to ensure nobody ended up at a 404 page unexpectedly.
  • Redirecting between routes that differed by more than the just the /2 prefix. An example of this was our 1-1s route. In the old UI, the route was ...latticehq.com/1-1s/{user_id}. In the new UI, it was ...latticehq.com/2/users/{user_id}/1-1s. Both of these made sense given how users navigated through each design. We had to handle instances of this on a case-by-case basis.

Conditional rendering in React

You might have noticed some things, like the 1-on-1s or Updates UI, still look the same and were just moved to a new location. While true, we also needed a way to make finer styling changes to them. We also decided against duplicating React components and conditionally rendering within components instead to preserve logic and prevent regressions.

After fetching feature flag data from GraphQL, we cache it on the frontend to avoid re-querying for the data many times. And then use that logic wherever needed to determine how to render our React components.


Retrospection and Tradeoffs

All things considered, we're quite happy with the end result. The reception by our users of the new UI was positive, and we feel great about that! However, looking back, we pondered how the technical solution could have been improved.

We often thought about if using feature flags (and /2) was the correct decision or not. It initially seemed somewhat rudimentary, but it was also very practical and familiar to the entire team. At the end of the day, we still felt this was an effective solution. We had considered the idea of serving two separate React apps, but realized that would require us to make additional considerations with regards to the entire Engineering team's development workflow, our CI and deployment pipeline, and we'd likely face challenges with redirecting between the two apps as well.

The main area we called out for improvement was testing. To give some context, on the client side, we currently leverage Flow types (we just started migrating to Typescript!) and Cypress integration (or "end-to-end") tests to test critical product flows, like setting up a Review Cycle or submitting a piece of Feedback. Cypress tests would have adequately tested our redirect logic, but it also felt inappropriate to use them test every redirect. We were also iterating quickly, and wanted to be careful about creating tests that could eventually become obsolete.

What's Next?

Project Elevate was a massive project, and we're super excited the new UI is in your hands now. It took us 6 months, a team of 6 engineers and a ton of customer research and planning from our designers, PM, engineering manager, and many others to build and roll out.

One of our company values at Lattice is "What's next." In its spirit, we're already planning how to leverage the new navigation to evolve the app. Over the next year you can expect us to continue iterating based off your feedback, polishing and building off the new improved UI, and potentially introduce some brand new tools 😉.

We're also going to remove the /2 from the URL and remove the Project Elevate feature flag from our code. Some of the major things we need to do are:

  • Update our routes and files to render new UI on / routes, and delete /2 routes
  • Update redirect logic
  • Resolve conditional rendering logic

Lastly, we're going to send out some awesome swag to the team!

Are you interested in working on challenging technical projects like Elevate? Check out our open engineering roles! We're hiring! 🙂

more articles
About Lattice

Our mission is to make work meaningful

Lattice works with People teams across the globe to turn employees into high performers, managers into leaders, and companies into the best places to work.

Join us