Planning the Refactor: Scope, Communication, and Standards
Once management has signed off on a CSS refactoring project, the real work begins. The team must align on internal code standards and best practices, define the strategy, and break the work into concrete, testable tasks. This planning phase also requires setting up a visual regression testing suite and a maintenance plan to keep the refactored codebase healthy over the long term.
Before any code is touched, the team needs to revisit the CSS health audit data — file sizes, selector complexity, duplicated declarations — and discuss how to tackle each issue. These discussions are important because the specific weaknesses in the codebase will shape both the refactoring approach and the testing strategy. Documenting internal rules and standards keeps everyone on the same page, reducing the risk of introducing inconsistencies while refactoring.
Task definition and deadlines must be realistic. The team should account for ongoing feature work and urgent bug fixes so the refactoring effort doesn't block critical tasks. Since refactoring rarely produces visible front-end changes, management can't easily track progress on its own. Transparent communication is essential here. Team members should keep stakeholders updated through project stand-ups, shared channels, and collaborative tools like Miro or MURAL. This visibility also helps justify the resources spent when results aren't immediately obvious to non-technical stakeholders.
"Communication and clearly making the progress and any upcoming issues visible to the whole company were our only weapon. We decided to build up a very simple Kanban board, established a project stand-up and a project Slack channel, and kept management and the company up-to-date via our internal social cast network."
The single most critical factor in planning is keeping task scope as small as possible. Smaller tasks are easier to manage, test, and integrate without destabilizing the codebase.
Harry Roberts calls these well-bounded efforts refactoring tunnels. A large-scope task — say, converting the entire codebase to BEM methodology at once — might appear to be a simple find-and-replace exercise. In practice, it touches every element on every page, breaks unpredictably, and offers no clear endpoint. Teams can spend days or weeks in a seemingly endless tunnel, accumulating technical debt instead of reducing it, and often end up abandoning the effort entirely.
A narrower task like refactoring just the navigation component is far more achievable. It has a clear finish line: once the component is refactored and its tests pass, the task is done. Even if complications delay progress, the work remains tractable and the team always knows what "done" looks like.
With the scope defined, the team must agree on both a refactoring strategy and a regression testing method before diving into individual tasks. The chosen strategy should minimize the risk of breaking existing functionality while delivering measurable improvements.
The next step is choosing among incremental refactoring strategies that prioritize safety and visibility, which we'll explore in detail.
Refactoring One Component At A Time
A full rewrite of a CSS codebase is rarely a good idea. The risk of regressions, accidental deletions, and style conflicts grows alongside the size of the change. An incremental strategy, where each refactoring task stays small and isolated, is a safer path forward.
Harry Roberts outlined a granular approach to CSS refactoring back in 2017, and it remains a reliable method. The idea is to move through the codebase component by component, starting with low-scope tasks and working up to global style changes only once the individual pieces are stable.
Build The New Component In Isolation
Start by picking one component and rebuilding it outside of the legacy codebase. Because component styles usually mix class-based rules with global element selectors, problems can come from either side. Class selectors might be overly specific, hard to reuse, or tightly coupled to a particular HTML structure. Global element selectors, meanwhile, can be greedy and leak unwanted styles into multiple components, forcing high-specificity hacks to undo them.
An isolated environment keeps the new code clear of these conflicts. You do not need a full build system for this; a simple tool like CodePen is sufficient. To further separate the new code and prevent collisions with existing class names, prefix all new class selectors with rf-.
While rebuilding the component, you also get a clean chance to improve the markup: remove unnecessary wrappers, rename classes for clarity, and add ARIA attributes where needed.
Integrate, Override, And Validate
Once the isolated component is ready, replace the legacy markup and append the new CSS to the existing stylesheet. Resist the urge to delete legacy styles at this stage. A flood of simultaneous changes makes it impossible to pinpoint which one caused a regression if something breaks. The .rf- prefix stays in place here to prevent direct conflicts with the old codebase.
Legacy component styles and global selectors will still leak unwanted styles into the refactored component. The old code often included specific, sometimes faulty, rules to cancel those side-effects; the new code should not inherit such hacks. Since the legacy selector that causes the damage is likely used by many other parts of the project, do not edit it directly yet.
Instead, create a dedicated interstitial file — name it overrides.css or defend.css — for high-specificity rules that neutralize the legacy bleed. These overrides are temporary and exist solely so the new component renders correctly alongside the untouched legacy code.
overrides.css to combat the unwanted side-effects. This file contains high-specificity code that overrides the legacy styles. (Large preview)If rending is off, go back to the isolated env to confirm the component markup and styles are complete. If they are, inspect what else from the legacy file might be bleeding in. Once the component behaves correctly with the overrides in place, remove the matching legacy code for just that component. Then drop the now-unneeded hacky rules from overrides.css and check if anything beneath the component has changed.
overrrides.css which helped combat the side-effects from those selectors. However, global CSS selectors may still apply unwanted side-effects so we cannot completely remove this file until we’ve refactored global styles also. (Large preview)Some overrides will cling on. This typically happens when a global element selector leaks styles into multiple components that you have not yet refactored. Do not expand the scope of the current pull request to fix them. Keep the override as a handy TODO marker, update your task tracker, and leave a useful comment in overrides.css for the rest of the team. When all components have been touched through this process, global-selector tasks become much less risky because they only affect one codebase, not two.
/* overrides.css */
/* Resets dimensions enforced by ".sidebar > div" in "sidebar.css" */
.sidebar > .card {
min-width: 0;
}
/* Resets font size enforced by ".hero-container" in "hero.css"*/
.card {
font-size: 18px;
}
Repeat Until The Temp File Is Empty
After the component integrates cleanly, open a pull request and run a visual regression test against it. This automated check is the final safety net before merging into a main branch. Then repeat the cycle: pick the next component, build it in isolation, merge, fix, test, and merge.
After the last component is done, most if not all of overrides.css
will reference wide-reaching element selectors. This is where the strategy moves from individual components to two-cross-cutting global styles.
Cleaning Up Global Styles Last
With every component already shielded from global side-effects via the overrides file, higher-scope style work is no longer high-risk. You can now safely refactor buttons, links, form elements, containers, and grid systems to remove duplicated component-level declarations.
Follow the same pattern: build the new global styles in isolation, bring them into the project, and watch for new conflicts. As you delete old global CSS rules, you will notice the overrides (which were compensating for them) can also be removed. That way the project shifts step by step from a hacky legacy baseline toward a single consistent set of global styles.
There is the important caveat that comes with isolated development, though. Shared pieces — inputs, headings, common layouts — are built with us having any view of the legacy style guide. Adding them in isolation means creating duplicates that mimic an outdated system, which is not desirable. This is precisely why shared styles are pushed to the end. The codebase is already healthier by then, and it’s easier to spot overlap between components and replace it with deliberate utilities and distinct design-token styles rather than patch the legacy mess.
overrides.css to remove unwanted side-effects of the legacy codebase. (Large preview)If new global styles cause side-effects, the regression tooling and the overrides file will surface them. Once the stylistic debt is paid down and the overrides file is empty, it gets deleted, leaving a clean and functional CSS codebase behind.
overrides.css once the codebase has been completely refactored. (Large preview)Working Through a Refactor, Component by Component
A practical way to see incremental refactoring in action is to trace it through a small page. Imagine a layout with a title and a couple of cards sitting inside a two-column grid. Each card holds an image, a title, a subtitle, a description, and a button. The existing CSS is far from clean: specificity battles, overrides, duplicated rules, and a few places where styles are awkwardly undone.
See the Pen [Refactoring CSS — example 1](https://codepen.io/smashingmag/pen/KKmRaQJ) by Adrian Bece.
h1, h2 {
margin-top: 0;
margin-bottom: 0.75em;
line-height: 1.3;
font-size: 2.5em;
font-family: serif;
}
/* ... */
.card h2 {
font-family: Helvetica, Arial, sans-serif;
margin-bottom: 0.5em;
line-height: initial;
font-size: 1.5em;
}
The .card selector reaches deep into the markup with high specificity, locking in a rigid HTML structure and letting card styles bleed into any nested element.
/* Element needs to follow this specific HTML structure to have these styles applied */
.card h2 > small {
/* ... */
}
/* These styles will leak into all div elements in a card component */
.card div {
padding: 2em 1.5em 1em;
}
/* These styles will leak into all p elements in a card component */
.card p {
font-size: 0.9em;
margin-top: 0;
}
The safest starting point is the lowest-scope, topmost child component. Here, that means the card itself. Build it in isolation first, applying the standards the team has agreed on. Switch the broad legacy selectors for simple, single-class BEM selectors, and replace hard-coded color values with CSS custom properties. Add some temporary helper CSS during development that won’t be copied into the real codebase.
See the Pen [Refactoring CSS — example 2 (isolated card component)](https://codepen.io/smashingmag/pen/JjNvELK) by Adrian Bece.
New classes get an rf- prefix to avoid clashes with existing styles and to make it obvious which parts of the page are already refactored. That visibility helps with debugging and progress tracking.
.rf-card {
color: var(--color-text);
background: var(--color-background);
}
.rf-card__figure {
margin: 0;
}
.rf-card__title {
line-height: 1.3;
margin-top: 0;
margin-bottom: 0.5em;
}
Swap the legacy card markup for the new markup and add the fresh styles to the stylesheet. Leave the old card CSS in place for now. The next step is checking for side effects: do any other legacy selectors reach into the refactored card and mess with its appearance? In this case, yes. A broad element selector resets the title font properties, and another wide rule changes the font size within the card.
.grid {
/* ... */
font-size: 24px;
}
h1, h2 {
/* ... */
font-size: 2.5em;
font-family: Georgia, "Times New Roman", Times, serif;
}
See the Pen [Refactoring CSS — example 3 (merge legacy and refactor)](https://codepen.io/smashingmag/pen/NWjMdYO) by Adrian Bece.
Fix those leaks with targeted overrides in an overrides.css file, and comment each one so everyone knows which legacy selector caused the problem. Those comments are effectively a TODO list: the culprits are the .grid component and global h1, h2 element selectors. They are objectively faulty rules that get reset in most contexts, so they’re worth fixing rather than patching around.
/* Prevents .grid font-size override */
.rf-card {
font-size: 16px;
}
/* Prevents h1, h2 font override */
.rf-card__title {
font-family: Helvetica, Arial, sans-serif;
font-size: 1.5em;
}
See the Pen [Refactoring CSS — example 4 (adding overrides.css)](https://codepen.io/smashingmag/pen/zYwjNjG) by Adrian Bece.
Now the old card styles can come out. The override file stays intact because those leaked rules came from other components, not the card’s own CSS.
See the Pen [Refactoring CSS — example 5 (Removing legacy component styles)](https://codepen.io/smashingmag/pen/yLbjgjW) by Adrian Bece.
The TODO list points to two options: refactor the grid (a shorter tunnel, since it’s lower in scope) or tackle the global element selectors (a longer one). Pick the grid. Develop it in isolation too — there’s no need to include card styles for that work, since the card won’t interfere with building the grid in a vacuum.
See the Pen [Refactoring CSS — example 6 (refactoring grid component)](https://codepen.io/smashingmag/pen/JjNvEZK) by Adrian Bece.
Replace the grid markup and add its new CSS to the codebase. Check for any new conflicts this introduces.
See the Pen [Refactoring CSS — example 7 (merged grid component, updated overrides.css, removed styles)](https://codepen.io/smashingmag/pen/KKmRaeE) by Adrian Bece.
Nothing new breaks, so delete the legacy grid styles. Check overrides.css for entries tied to the old grid selector. The documentation pays off here: the grid-related override can be removed safely, and the remaining entries point squarely at the heading element selectors. Repeat the same isolation-refactor-integrate cycle for those.
/* Reset .grid font-size override */
.rf-card {
font-size: 16px;
}
<h1 class="rf-title rf-title--size-regular rf-title--spacing-regular">Featured galleries</h1>
.rf-title {
font-family: Georgia, "Times New Roman", Times, serif;
}
.rf-title--size-regular {
line-height: 1.3;
font-size: 2.5em;
}
.rf-title--spacing-regular {
margin-top: 0;
margin-bottom: 0.75em;
}
Verify no new issues appear, then strip out the legacy h1, h2 selector and clean the now-empty entries from overrides.css.
See the Pen [Refactoring CSS — example 8 (element selectors)](https://codepen.io/smashingmag/pen/VwbxPBW) by Adrian Bece.
The page is now running on refactored card, grid, and title components. The codebase is noticeably more consistent, and adding new grid items or title variants no longer requires undoing stray styles.
One pass isn’t necessarily complete. Building components in isolation often means rebuilding pieces of the style guide over and over. The card styles, for example, probably contain button styles that belong in a shared component. If other cards or elements use the same button look, refactor those duplicated rules out into a standalone .button component. The legacy .button selector can be replaced in the same sweep.
/* Refactored button styles scoped to a component */
.rf-card__link {
color: var(--color-text-negative);
background-color: var(--color-cta);
padding: 1em;
display: flex;
justify-content: center;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
Moving up the scope ladder is more manageable now that the foundation is stable. The risk of a top-level change breaking a lower component is gone. Apply the same incremental cycle: rebuild the button in isolation, swap the markup, add new styles, run a quick conflict check, then remove the legacy button CSS and the component-scoped duplicates.
/* Faulty legacy button styles */
.button {
border: 0;
display: block;
max-width: 200px !important;
text-align: center;
margin: 1em auto;
padding: 1em;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
font-weight: bold;
text-decoration: none;
}
.cta {
max-width: none !important;
margin-bottom: 0;
color: #fff;
background-color: darkred;
margin-top: 1em;
}
.rf-button {
display: flex;
justify-content: center;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.rf-button--regular {
padding: 1em;
}
.rf-button--cta {
color: var(--color-text-negative);
background-color: var(--color-cta);
}
/* Before - button styles scoped to a card component */
<a class="rf-card__link" href="#">View gallery</a>
/* After - General styles from a button component */
<a class="rf-button rf-button--regular rf-button--cta" href="#">View gallery</a>
See the Pen [Refactoring CSS — example 9 (final result)](https://codepen.io/smashingmag/pen/yLbjgqd) by Adrian Bece.
Once the whole refactor lands, a project-wide search-and-replace removes the rf- prefixes. Renaming classes is straightforward now, and keeping the prefixes forever would only cement a naming scheme that’s meaningless once the legacy code is gone.
Regression Testing During a Refactor
Even a disciplined incremental process can let bugs slip through: conflicting styles, missing rules, or leaked selectors. Automated visual regression tools like Percy or Chromatic catch those issues at the Pull Request level. They snapshot pages and components, compare layout and styling changes, and flag anything unexpected before it ever touches the production site.
A CSS refactor should not change the way a page looks, so the testing workflow can often be reduced to a simple question: did anything visually change, and was that change intentional? Tools don’t need to be heavy either. The team refactoring the Sundance Institute’s CSS generated a static style guide page with Jekyll and ran their tests against that, which doubled as a living reference for the team and outside vendors.
“One unintended consequence of executing the refactor in abstraction on a Jekyll instance was that we could now publish it to Github pages as a living style guide. This has become an invaluable resource for our dev team and for external vendors to reference.”
After the refactor is in, running an A/B test against the old code can prove the payoff. If the project goal was cutting CSS file size, those gains can be stark, especially on mobile. Trivago released their migration as an A/B test and saw enough of a positive mobile response to accept the change after four weeks.
“(…) we were able to release the technical migration as an A/B Test. We tested the migration for one week, with positive results on mobile devices where mobile-first paid out and accepted the migration after only four weeks.”
Tracking Progress and Keeping It Clean Afterward
Kanban boards and GitHub issues handle task tracking well enough, but they’re weak for a quick “what’s left on this page” check. That’s another argument for the rf- prefix, which Harry Roberts has covered in depth. He notes that the prefix doesn’t just separate new code from old — it gives stakeholders a per-page view of how far the work has gone.
If management wants to deploy only the refactored homepage early, developers can see exactly which homepage components are still legacy. A quick temporary CSS rule can highlight refactored and unrefactored areas on the page itself, making it trivial to reprioritize tasks.
/* Highlights all refactored components */
[class*="rf-"] {
outline: 5px solid green;
}
/* Highlights all components that havent been refactored */
body *:not([class]) {
outline: 5px solid red;
}
Once the refactor is done, the work shifts to protecting the codebase from decay. Rushed features and quick fixes will accumulate technical debt. That debt should be quarantined into its own file, commonly named shame.css, so it’s visible and documented rather than silently mixed into the good code.
Write down the rules and standards established during the refactor. A document that spells those out makes code reviews consistent, gets new developers up to speed faster, and smooths hand-offs. Automated tools like stylelint can also enforce a lot of that policy without relying on memory or vigilance. Andrey Sitnik, author of PostCSS and Autoprefixer, points out how much easier that makes life for everyone.
“However, automatic linting is not the only reason to adopt Stylelint in your project. It can be extremely helpful for onboarding new developers on the team: a lot of time (and nerves!) are wasted on code reviews until junior developers are fully aware of accepted code standards and best practices. Stylelint can make this process much less stressful for everyone.”
A Pull Request template with a standards checklist and a link to the project’s written code rules can push even more of that responsibility onto the developer before the review even starts.
A Component-By-Component Path To Cleaner CSS
Refactoring CSS incrementally remains one of the safest strategies a team can adopt. Instead of attempting a full rewrite, the work is split into discrete, low-scope tasks that target one component at a time. Each component is developed in isolation, away from the legacy code that may interfere, and then gradually merged back into the main codebase.
When the newly refactored component conflicts with the existing styles, a temporary CSS file can hold the necessary overrides to remove those style clashes. Once the legacy code for that component is removed, the next component is tackled. This cycle repeats until the entire codebase is refactored and the temporary override file is empty.
Verifying Changes Before Deployment
Visual regression testing tools like Percy and Chromatic play a key role in this workflow. They detect regressions and unintended visual changes at the Pull Request level, allowing developers to address problems before the refactored code reaches production.
In addition, A/B testing and monitoring tools help confirm that the refactoring efforts do not degrade performance or user experience. Only after these checks pass should the refactored project be launched on the live site.
Keeping The Codebase Healthy
Once the refactoring work is done, maintaining the project becomes an ongoing concern. The team must continue to apply the agreed-upon standards and best practices throughout the codebase. Doing so preserves code health and quality over time, preventing the need for another large-scale cleanup in the future.



