How to Refactor 17K Lines of CSS

Table of contents

Many of us used to work on big development projects. I'll describe the one, which lasts for already 15 years. It contains a few dozens of ASP.NET Web Forms applications. The main app contains about fifteen hundreds of ASPX pages.

Why so much? The desire to fit customers' needs from one side and variety of possible requirements from another can be a reason. Each customer wants its own specific piece of functionality and sometimes gets it. But let's move on. Besides the large number of pages, we had lots of CSS styles.

In the beginning, there was one small web project. It had some CSS. CSS file was not too large and it was readable enough. As time went on, people came and created some new feature with adding some selectors into the CSS file. It didn't look too scary, because it was still possible to read and understand the contents.

Some years after our features became more and more complicated. Basic styles of controls were overridden on different pages because of respective design requirements. In addition, the same CSS classes were reused on several different pages without any common semantic. At some point, our CSS turned into 17 thousand lines of trash, which didn't seem to be understood by anyone.

To make it even more complicated it was decided to make custom skins (separate styles) for different customers. If you look at the visual difference you would not expect much code in those skins, because there is not much things are supposed to be different (there were mostly foreground, background and font colors). In practice stylesheets for custom skins weren't too small, but contained lots of CSS. Some of it could probably be common for all though.

The problem

We knew that despite of the fact of visual design integrity the project had many exceptions, e.g.:

  • 12 different grids which looked almost similar, but not exactly.
  • 4 variations of toolbars with 3 variations of buttons with drop-down lists. Drop-downs looked suspiciously similar to each other as well.
  • There were 2 different feedback controls. Each of those had only some text with an icon on the left.

I would remind that we had several web projects, not the only one. Controls with stylesheets for each of them were developed in parallel for quite a long time before someone realized that it would be possible to join them together and move into a new common control library. It could have some common controls with common stylesheets, which could be overridden inside each web project. The idea turned into reality after some time, but it didn't mean that all copy-pasted code was removed or everything was unified. In fact we got even more controls to support. A new grid developed in common library didn't replace all of 12 existing grid variations in hundreds of usages in the code. That was predictable since no one likes to spend much effort on refactoring without any functional change. After some time developers were hard to choose the "right" control from the variety we had. In addition, each of our existing controls could implement some specific functionality-related behavior. Our designers weren't very consistent with their needs from time to time.

At some point we even faced the well-known limitation of 4095 CSS selectors per file in Internet Explorer 9 (as a workaround we split it into two).

One day it was decided to refresh application design. Everyone understood that we need to spend some time on refactoring. The whole CSS. The mess we were in couldn't last longer, even product owners had the same thoughts. So we got an approval and started working.

Let's begin

Well, it's great to refactor anything especially if you got an approval. Let's look what we had.

  • One enormous CSS file with 17 thousand lines of code for main web application.
  • More than 50 custom skins, one CSS file per each. In theory they could contain only small difference, but in practice they had quite large pieces of common copy-pasted code.
  • CSS file of common library, which was used by main web application and by other apps as well.
  • CSS files of other apps. They didn't had so much code at least, but of course some of their code was copy-pasted.

We started from removing unused classes. That's not so obvious as removing unused code on C# especially with some tools for solution analyzer. The only way to go was searching class name as text in the whole solution. If you'd asked to clean up 17 thousand lines of CSS in solution of 250 projects. Order SSD drive for yourself. The process would go faster.

If your project is in the beginning of its life, please use these pieces of advice:

Always use full class name in your code

Never split it into several words, e.g.:

public const string ProgressBar = "progressbar";
public const string ProgressBarSmall = ProgressBar + "small";

You could consider .progressbar as still in use and .progressbarsmall as safe to remove, which is false positive. You have to reintroduce it back if you even notice it's gone. Quite sure what's used and what's not? That's not a point for project with fifteen hundreds of pages and hundreds of settings, which turn on and off different features.

Don't write overengineered code like that:

public class Feedback
   public static string CssClass = GetType().ToString();


First, it doesn't work for inheritors (in this case you should replace GetType() with typeof(Feedback)). Second, it's not possible to find the CSS class using text search.

Just keep text. Use text search in solution with preserve case and whole word. Don't make it complicated.

Use prefixes

I would describe a bit later that it's very easy to split the CSS into modules by giving them different prefixes.

It's almost impossible to search the usages of the following CSS classes:


It becomes much better if you use prefixes. First two classes could be treated as helpers. Let them be .h-hidden and .h-visible. It's already possible to search. For any specific classes it's possible to use project name as prefix. MyProject project would have its classes having .mp- prefix, e.g. .mp-login-page. Common controls could have .ct-.

Some 3rd party libraries already use prefixes in their CSS. When you see such styles, you can always understand which library or application it belongs. Also this approach eliminates the small risk of names overlapping.

Don't use the same class names for different purposes

E.g. .error and .text classes might have reused many times in any single application.

.mp-login-page .error
.ct-feedback .error
   background-color: red;

I would replace them into the following:

   background-color: red;

Too large? True. But it's searchable and removable enough.

Make a list of CSS classes

Put the list of CSS classes into a wrapper. In C# there can be a static class containing not all but at least those, which are frequently used. It will help to search, rename and remove classes later.


My uncle was always saying that you need to build anything with the plan of destroying it afterwards.

Do you think you never will, because your project is very small? We thought the same before ours became a monster very large. After all the removal of unused classes is quite common case. We had lots of them. Sometimes we could remove 100 lines in a row. After we finished, we were able to reduce the length from 17 to 16 thousand of lines. Do we use all of that?! We do! Can't remove anything else anymore.

What's next? It was found out that we used 100% of remaining CSS classes after a cleanup. When we performed removal, we set our eyes on the following selectors (usually used once only).

   margin-top: 10px;
   padding-left: 15px;

That looked quite stupid. Why did we have to store lots of CSS classes containing only some margins and paddings? What could we do with them? The idea was borrowed from Twitter bootstrap library. They used helper classes for some predefined margins. We could use helper class .h-mt10 for margin-top: 10px and .h-pdl15 for padding-left: 15px;. That helped to clean up some more.

Then we searched through similar CSS code. The most popular thing was the style of hyperlink with an icon on the left.

   text-decoration: none;
.ct-imagewithtext img,
.ct-imagewithtext span
   vertical-align: middle;
.ct-imagewithtext img
   margin-right: 4px;
.ct-imagewithtext span
   text-decoration: underline;

I suppose there were about 20 different variations of that similar thing. But every new occurrence had some new class name with little or big style difference. We started to unify all of those similar styles, so the app became looking nicer. Small differences began to fade away.

We managed to refactor similar controls with completely different styles by removing rarely used ones and replacing them with others. If there were lots of usages we took styles from the best control and applied them for others. Unfortunately, we couldn't remove everything.

Finally, we moved to common library. As I already mentioned, the common CSS contained only one part of styles. Second part was redefined in each web application we had. That was not a good idea since almost everything was overridden the same, in fact it was copy-paste. However not 100% of copy-paste. Previously when there was any design change (e.g. small color adjustment), it could occur in the only one specific app ignoring the rest of them. On the refactoring stage we moved all overrides into the common part killing lots of copy-paste. Apps became nicer, their toolbars, grids and other controls became looking much more similar.

Next step was moving so-called CSS Reset into common library. Only two apps had that before, of course with some different implementation. We took Normalize.css as a base, included it into common library. In addition, we moved there some base styles for body element (e.g. font family, size, line-height and some more).

At that time, we still trying to remove copy-paste and unifying styles. At the end, we started to work with custom skins. They didn't change much visually, but they contained lots of styles by some reason. There was about 50 skins. Older ones were written manually, newer ones used LESS. There was no any common template though. CSS generation performed manually, and generated result was concatenated with some CSS part. We started with that one and moved it into common area. Also we set up automatic CSS generation by LESS using lessc tool. Then we moved to older skins where LESS was not in use.

Despite of the fact that old skins contained lots of CSS, it was found out to be more or less copy-pasted from common part. At the end, we extracted common CSS part, small template for 40 lines of code and 50 less files with 15 variables each for customization.

Common part of custom skins was moved into the end of famous 16-thousand-lines CSS. Since selectors with the same specificity can override each other based on the location in CSS (the last one wins) it made sense. Later on we used Web Essentials. That's a Visual Studio add-on which helped us much. It can find syntax errors in CSS or LESS files and provides some tweaking features as adding missing vendor prefix. It can also find similar CSS selectors inside one file. It turned out that we had the following case quite often:

There is a selector defined on line 5255:

   margin-top: 15px;
   background-color: blue;

Then it's overridden on line 13467:

   margin-top: 10px;
   background-color: green;

It did happen quite often. Sometimes there were up to four overrides of single selector. WebEssentials was constantly complaining on such things, so all of them were found and removed. As I said the last selector with the same specificity wins, so we were removing occurrences from the top. That's a bit risky when there's lots of classes put onto a single element. Sometimes we had to move selectors around, and their precedence could be changed. But that's nothing to do about. During the process, QA had to walk through all pages in app and compare the look with production environment.

At some point, we were ready to split the CSS with 16 thousand lines into a smaller parts. They were 120. During the prebuild step, they were joined into one. After some time we moved the code into LESS.

How it works now

Let's look into the small example.

Since that's just an example let's simplify the task a bit and imagine that we have the following projects. Common library (CommonControls), the separate project for everything in Content delivery network (CDN) used in main web app.

Common Library folders

Common library contains LESS files, which are joined together into one (common-controls.less) and then converted into CSS (common-controls.css).

Common Controls folder

Let's look into the content a bit deeper.

  • 01-essentials.less stores only LESS variables and mixins. They're used by LESS files of common library as well and also by LESS files of any other web application.
  • 02-normalize.less. That's slightly changed version of normalize.css.
  • 03-default-styles.less stores common styles of all applications including body element, fonts and colors.
  • 04-helpers.less stores so-called helpers, e.g. margins and paddings.
  • Then there are stylesheets of each common control.

Perform a setup of build events for CommonControls project. I'll move everything into a separate command file for easier edit and merge the project file later on.

Pre-Build Event Command Line

The script is simple. Every LESS file from Stylesheets folder is joined together. The combined result is moved into CombinedStylesheets. Then run LESS preprocessor and get the final CSS.

set ProjectDir=%~1

copy "%ProjectDir%Stylesheets\*.less" %ProjectDir%CombinedStylesheets\common-controls.less

call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%CombinedStylesheets\common-controls.less %ProjectDir%CombinedStylesheets\common-controls.css

Let's look into stylesheets of Cdn project. _cssparts folder contains all styles belong to the app. They're joined into combined.less file. In the real project there are lots of files. The screenshot shows simplified version of it.

Cdn folder

The order of files doesn't make much sense except the first and the last ones.

001-imports.less contains the following code:

// Importing LESS template from CommonControls
@import "../../CommonControls/Stylesheets/01-essentials.less";

// Usual CSS import
@import "common-controls.css";

The first directive performs an import of LESS file content (here 01-essentials.less). That's the similar as if we just concatenated it with the others while combining. Importing of essentials file allows us to reuse every variable and mixin we defined in CommonControls project. The second directive is the classical CSS import. Generated CSS file will contain that line as is. Despite the fact that CSS imports are not recommended to use, and the only reason why it's here is IE9.

z-ie9-special.less contains the only selector, which should be the last one in the generated file. It's used on a special page, which goal is to understand if the selector is applied or not. If the common number of selectors are more than 4095, the selector will not be applied. Then we have to split the CSS once more. In fact we gave up combining common library CSS with main web app CSS.

The following things happen on pre build:

@REM Copy common controls stylesheet
COPY %ProjectDir%..\CommonControls\CombinedStylesheets\common-controls.css "%ProjectDir%Skins\common-controls.css" /Y

@REM Combine CDN LESS files and run preprocessor
copy "%ProjectDir%Skins\_cssparts\*.less" %ProjectDir%Skins\combined.less
call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%Skins\combined.less %ProjectDir%Skins\combined.css

Combined common library CSS and web project CSS are moved into Skins root folder. In real projects it's better to use any other specific tools for combining CSS files and avoid simple concatenation, but that's just an example.

Let's look into custom skins generation.

Skins folder

_custom-parts folder contains the template for generating each skin - custom-template.less. Let's assume that for now we need to customize H1 and H2 colors. custom-template.less will contain the following:

   color: @h1Color;
   color: @h2Color;

Default-values.less contains the default values of all the variables. That will allow not to define the value of each of them if there is no need to override.

@h1Color: #F58024;
@h2Color: #E67820;

Typical skin (skin.less) contains the following:

@import "..\_custom-parts\default-values.less";

@h1Color: #000;
@h2Color : #707050;

@import "..\_custom-parts\custom-template.less";

First we import default values, perform some overrides and then import the template.

Let's write the following pre build event to generate everything:

@REM Regenerate customskins using their LESS templates
for /r "%ProjectDir%Skins\" %%i in (*.less) do (
   if "%%~nxi"=="skin.less" call "%ProjectDir%..\Tools\lessc\lessc.cmd" "%%~dpnxi" "%%~dpni.css"

There will be skin.css file together with each of skin.less. For the LESS file above the following CSS is generated:

   color: #000000;
   color: #707050;

When we moved to LESS we didn't change much (except custom skins). LESS code looked almost the same as ordinal CSS. With a small exception of some parser errors, e.g.:


margin-top: -4px\0/IE8+9;

Not sure that IE hack should look like that, but anyway. LESS allows you to escape your input using the following characters ~"":

margin-top: ~"-4px\0/IE8+9";

Any other code was valid enough for LESS. After some time some simple variables started to appear:

@сtDefaultFontSize: 14px;

Then some more complicated mixins:

   position: absolute;
   left: -10000px;
   top: -4000px;
   overflow: hidden;
   width: 1px;
   height: 1px;

The mixin above is used by helper class, but also by some other classes. We had an interesting case to use LESS variables and mixins. Since we were unable to replace grids and some other controls by common library version, we started to use those variables and mixins for storing common styles and colors. It turned out that LESS is even more applicable for old projects with lots of style copy-pasting. Anyway there are lots of interesting cases for it. E.g. generation of background-images for various button types using sprite:

.ct-button-helper(@index, @name, @buttonHeight: 30, @buttonBorderThickness: 1)
   @className: ~".ct-button-@{name}";
   @offset: (@buttonHeight - 2*@buttonBorderThickness - @buttonIconSize) / 2;
   @positionY: @offset - (@index * (@buttonIconSize + @buttonIconSpacingInSprite));

   @{className} { background-position: 8px unit(@positionY, px); }
   @{className}.ct-button-rightimage { background-position: 100% unit(@positionY, px); }

Example of its usage:

.ct-button-helper (0, "save");
.ct-button-helper (1, "save[disabled]");
.ct-button-helper (2, "cancel");
.ct-button-helper (3, "cancel[disabled]");

It's also good to generate font declaration CSS as well, however an implementation can tightly depend on a specific font.


Let's take a brief look into what we did.

  • Lots of unused CSS selectors were cleaned up.
  • Lots of selectors used only once in code were replaced with helper classes. Actually that was quite large part of everything we removed.
  • Common CSS part of custom skins was moved into the end of main CSS file. That was needed to be done before next steps occur. Also LESS was used for generating custom skins.
  • Lots of overrides of a single selector in the same file were removed with a little help of WebEssentials add-on. That was needed to be done as well before next steps occur.
  • Main CSS was split into a smaller parts for easier edits. These parts are combined into one file during the build.
  • Common controls overrides were moved from different apps CSS into common library CSS.
  • Common parts (normalize.css, font styles and colors) were moved into common library since we use the same style everywhere across all the apps.
  • LESS is in use in every app now.
  • Common LESS variables and mixins were extracted from frequently used styles. They are available in all apps we have.
  • A C# wrapper (just a static class with static properties) was made to store frequently used CSS classes. That doesn't make sense for classes, which are used only once. A wrapper is mostly done for frequently used classes.
  • We started using prefixes for CSS classes. We couldn't use them everywhere, but at least we try to use them for every new class.

In general, we got the code back under control. There were no any big issues during the further design refresh. Good that we performed it carefully changing one component by another. Not sure if the code became ideal or if it was shorten significantly. Anyway, we have lots of code, which is sometimes hard to be understood. But the amount of copy-paste decreased as well as the number of visual differences in various parts of the project.

You Might Also Like

Blog Posts Story of Deprecation and Positive Thinking in URLs Encoding
May 13, 2022
There is the saying, ‘If it works, don’t touch it!’ I like it, but sometimes changes could be requested by someone from the outside, and if it is Apple, we have to listen.
Blog Posts The Laws of Proximity and Common Region in UX Design
April 18, 2022
The Laws of Proximity and Common Region explain how people decide if an element is a part of a group and are especially helpful for interface designers.
Blog Posts Custom Segmented Control with System-like Interface in SwiftUI
March 31, 2022
Our goal today is to create a Segmented Control that accepts segments not as an array, but as views provided by the ViewBuilder. This is the same method that the standard Picker employs.