We say a
lot about...
everything

A Smooth Ride: Replacing PontaHR’s Styling Framework
development

A Smooth Ride: Replacing PontaHR’s Styling Framework

Popular Articles

A Smooth Ride: Replacing PontaHR’s Styling Framework

How to organize and get approval for production-scale code refactoring.

Published on January 16, 2025

by

Filip BolčićFilip Bolčić

Filed under development

“Do you have experience working with CSS-in-JSS libraries?” asked the interviewer, my soon-to-be team lead.

I was applying for a position at my current company, Ars Futura. They were looking for a developer to bolster the frontend team working on PontaHR, an in-house HR solution. The team operated within a strict design system and wanted someone who could be easily brought up to speed. Having little experience, I was lost for words. Luckily, the interviewers didn’t take it to heart and the meeting ended on a positive note - I got the job.

The reason behind the question becomes obvious once you accumulate some experience; how we handle styling on the frontend is important. Building a production-ready app requires an abundance of styling directives that, when declared with CSS files in a traditional fashion, become notoriously difficult to manage.

The community conceived many solutions to this problem, and one of the more popular ideas that took hold was CSS-in-JS. Take a look at this post by Oleg Isonen which explains the key points succinctly.

We too have used CSS-in-JS successfully. In Ars Futura, the library most commonly employed on new projects is Theme UI, so this was the weapon of choice I finally encountered when starting work on PontaHR.

For the first three years, we used Theme UI for each and every styling concern we had. We carved out our design system with a low-level component library, implemented color modes, and used it for all the other myriad concerns that a fashionable, production app has to handle daily. And for the most part, it worked beautifully.

But then, suddenly, three years into the project, we decided to replace it with another framework. Given our working implementation, and being a small team with a product that was yet to hit liftoff, this may seem unnecessary. So why did we do it?

This post will explain our journey of code refactoring a CSS framework in a production application. Committing to a refactoring decision at this scale is hard; there are always other priorities and it can be difficult to get the whole team on board. But, if the reasoning behind the decision is good, the application will profit in the long run. Our aim is to display our thought process in order to help other developers and decision-makers facing similar conundrums. 

But before we do, let’s talk a bit about the platform we were, and still are, building.

What is PontaHR and why are we building it?

In 2022, our company experienced a growth spurt, almost doubling its employee count within a year. We needed more governance, and even though content with our home-grown HR administration solution, we quickly outgrew it. 

Most of the software on the market had an overbearing amount of features,  better suited for an enterprise than an agile, fast-growing company. We wanted simplicity and a software that feels efficient, taking care of only the essentials - but taking care of them well.

In the end, we decided to build one ourselves – and PontaHR was born. It is a comprehensive human resource management solution geared towards small-to-medium companies. We aim to give fast-growing businesses a tool to manage their employees, enhance and grow their culture, and have fun while doing so. If you are interested, check out our website!

When we started working on the application, we decided on Theme UI because the team was familiar with it. Unfortunately, CSS-in-JS libraries rely on a couple of tricks that come with inescapable performance overhead. This can be seen once we examine how these libraries work under the hood.

Runtime & buildtime CSS

CSS-in-JS libraries enable us to write styling logic alongside our component logic. For example, this is how we can style a Header component with Theme UI:

When we run the application, the styling rules are generated in the document CSS source and applied to the proper elements. To someone encountering this for the first time, the results can feel like magic. How is the logic from our source code transformed to appropriate CSS? How does the application know where to apply them?

The reason is simple: CSS-in-JS libraries are heavily dependent on JavaScript.

This is a rough sketch of the steps the libraries have to take to generate the CSS (thorough explanation can be found on this link and also here):

  • parse the styling logic,

  • generate appropriate style rules,

  • bind them to their respective components via unique hashes,

  • inject them into the DOM.

The problem is that CSS-in-JS libraries like Theme UI do the work during application runtime.

This invariably hurts the performance of our application: it chucks extra work at the browser when there are thousands of other things happening. This is a widely recognized downside; there is a great investigation of CSS-in-JS pros and cons by one of Emotion’s most active maintainers which sheds more light on the topic.

As we rolled out features and our vision shaped, this situation kept nagging us. What will happen when PontaHR grows and there are additional UI components, each coming with its own heap of styling logic? Are we leaving a performance margin wide enough for lower-end devices? Finally, are we sacrificing the user for the developer experience?

Fortunately, there are other solutions that combat the same CSS issues while doing the compilation work outside of the runtime.

The trick is to move the processing work to application buildtime.

This is a rough sketch of how buildtime libraries work:

  • We write styling alongside our component logic.

  • The library scans it when the application is being compiled.

  • The styling logic is outputted to a CSS file,

  • which is appended to the generated HTML document. 

The important thing is that none of the work is done during application runtime. We get the same benefits of CSS-in-JS but with no runtime overhead. DX checked , performance checked .

Switching our styling framework from runtime to buildtime meant we could have the best of both worlds. With the appropriate library, we could keep our design system and development flow while ensuring a better experience for our users.

But, to do so, we would have to reconstruct the very foundations of our app. It would probably involve touching each and every file in our source code. 

Such an application-wide refactor is no small matter. Development teams are prone to shirking from them since they involve a cumbersome grind which takes time that could have otherwise been spent doing something more interesting.

Product owners and managers often cringe at the mention of refactors - especially if they don’t come from a technical background, and even more so if the issue is not immediately obvious and measurable. Their mindset is always geared towards providing immediate value to users.

In other words, the relevance of such a refactor should be evaluated carefully and honestly. But if it’s right, it's right, and the whole team should stand behind it.

When is a refactor right?

It’s common to procrastinate on refactor decisions, especially if they’re not mission-critical. There is also the refactor impact; small ones should be encouraged and done often, like dusting, to keep surfaces shiny. Big ones are scary and laborious - there should be a good reason to venture into them and it would require a coordinated team effort.

In other words, nobody likes doing them.

But, we should always keep aligning ourselves with the product’s mission and vision, which for us means delivering the best user experience. We realized there was room for improvement, and it became clear – we should do better.

There was another favorable circumstance that helped move the needle on this issue: the application was still in its relative infancy.

For an application-wide refactor such as this one, time is of the essence – every additional page, feature, or component will incur technical debt. If we know such a refactor is waiting for us around the corner, now is the best time to do it.

These insights, scope, relevancy, and time, are great compass points when evaluating refactor decisions. For us, even though we were vary of its scope, the importance and immediacy of this project convinced us that we should go for it. It just felt right.

Sweat the big stuff

We wanted to switch to a buildtime CSS framework. But which library to choose?

There are multiple approaches available, each with its merits and weaknesses. With so many options to choose from, it can be hard to decide, especially since it is related to such a fundamental application concern. Welcome choice overload .

Our approach was to focus on the big stuff and let the small issues slide. A different styling framework will invariably work differently, forcing the development team to adjust its routines. The key point to consider is whether the tradeoffs are good for the app. 

These were the main concerns we wanted to solve:

  • feature parity with our current solution (ease of use, theming, style colocation),

  • no runtime overhead,

  • future-proof,

  • good maintenance and track record.

In the world of software development, the technological landscape shifts rapidly. But, even though what is hottest changes often, fundamental tech, like HTML and CSS, doesn’t. So, betting on something closely related to these principals can add sturdiness to your stack.

Style co-location but used like regular CSS. A buildtime library with no overhead, and great maintenance. From these puzzle pieces an image emerged - we wanted to work with Tailwind.

Tailwind is a utility-first CSS library, which means it is based on small, laser-focused classes that handle very specific styling concerns. A preprocessor analyses the source code and produces a static CSS file with only those rules used in the project.

Even though Tailwind is one of the most widely used CSS frameworks today, it is not without its opponents. Its biggest asset is also its key issue – utility classes are laborious to write and can turn your code into an ineligible string soup.

Will using this technology require different ways of writing code? Sure. Is it uglier than our previous solution? Maybe.

The most important question is how big of a concern is liking this way of writing for the architecture of our application.

The big things - maintainability, features, performance - will keep the application running smoothly and should be key points of concern when evaluating a tech decision. Small things shouldn’t discourage it.

This doesn’t mean that the developer's voice is unimportant - these types of refactors require a coordinated effort and everyone should be on the same team. The approval required to drive such a change should not be strong-armed but earned by providing relevant information and discussing it as a team.

Team and stakeholder approval

Up to this point, the research was done mostly by one person. The development team knew a refactor could be in the works but the details were not yet shared. We assembled a meeting where we discussed a potential refactor from Theme UI to Tailwind. We analysed its pros and cons and evaluated the differences opposed to our current solution. Once the necessary data was laid out, it was easy to get everyone on board. The team was ready to hit the ground running.

Getting stakeholder approval is another enchilada. Product owners are not interested in technical details; value and time are their main currency. And when a refactor that would upend the whole app is proposed, your arguments better be good.

More concretely, these are the questions you need good answers to: what value to our users is provided with this change? And how much time would it take?

Our research showed better app performance but it was purely theoretical. It was time to transform this knowledge into something more tangible. We decided to construct a proof of concept by overhauling one of our simpler features - the login page.

This page looks deceptively simple. The truth is, in modern UIs, you’ll find a boatload of styles lurking underneath every component: hover and other states, responsiveness, and animations are some of the concerns that need to be handled in addition to base layout and styling.

Using one of the simplest pages made it possible to have a concept quickly, allowing us to examine both the development effort required to refactor at scale and the theoretical performance benefits.

I will not go into specifics here – suffice to say that our measurements showed performance improvements across the board.

We discussed the refactor again, this time presenting data to the whole project team: product owners, designers, marketing, sales, and development. The response was great and we got the green light.

Refactor with a plan

An application’s styling framework is the architectural bedrock on which every component, page, and feature is constructed. Changing it means pulling the bones out of your codebase and regrowing them as fast as you can as if using Madam Pomfrey’s Skele-Gro. Unfortunately, we couldn’t invest in a pure refactor sprint.

As I’ve stated before, the application was relatively young (it still is), and a lot of our energy was spent attracting clients and figuring out all the future directions in which PontaHR could evolve.

Providing maximum value to users is always a principal concern, but in this atmosphere, it's even more so. Being a small frontend team of three, we didn’t have the resources to lunge for it. This meant that the development had to occupy little pockets of spare time that were left after the main work was done.

We were in it for the long haul, and this knowledge shaped our refactor strategy.

Our application is built on a robust design system that includes primitives (such as colour, spacing, breakpoints) and atomic components (boxes, buttons, text) that combine to build complex UIs. Everything else is constructed on this foundation - every page is a collection of components and primitives arranged in layouts.

Our plan was to focus on the design system first. Since the existing code had a high level of reuse, refactoring it meant we would simultaneously strike at the concrete pages and features.

Working on such core features also meant we needed to be extra careful. Messing up a keystone component, like a simple button, would produce bugs throughout the app. So, our first few weeks were spent refactoring our design system and testing the changes meticulously.

Then we moved to concrete pages. Our plan was to start from simpler cases (like the login page) and move successively towards more complex ones (like the careers page, where users can collaborate and manage their listings).

The consequence of this strategy, where we updated piece by piece in spare hours, meant that our application supported two libraries and twin design system implementations. This bloat in our codebase didn’t worry us - it was a temporary, but necessary, tradeoff.

The real issue were the features that required the design system couple to live together peacefully, for example, dark mode. Since any page could consist of library A and library B components, both of them had to react accordingly to theme changes. But, because each library’s approach differed, we had to marry their implementations so the users would not experience any discrepancy.

We could go further into the specifics of how, but it is not really important. Every application, its implementation, and its use case is unique. When faced with the same problems, the steps your team will have to take will undoubtedly differ from the ones we had to.

The thing of note is to devise a plan early and stick to it. Huddle your team together and brainstorm the best approach. Assign roles, figure out priorities, and tackle this thing head-on.

A good strategy takes a refactor of this scale from scary and undetermined to something manageable.

A smooth ride to the future

And so, little by little, we managed to jettison the redundant library from our codebase. At our pace, it took approximately half a year (😅) to achieve what we set out to do.

That's a lot of time but it was well spent. A palpable feeling of satisfaction lingered when we watched the final bits of the existing code dissolve after the cleanup; akin to overhauling an aged motor on a cherished car with our own two hands.

We knew the driver was now in for a smooth ride.

Related Articles

Join our newsletter

Like what you see? Why not put a ring on it. Or at least your name and e-mail.

Have a project on the horizon?

Let's Talk