The problem using CSS in a large project is that over time the amount of CSS grows. When a rule is defined generically, like .shadedData, then it is difficult to know every place it is used. And when a developer doesn't know where it is used, modifying the style is done by adding a new rule .shadedData2. Even rules that aren't as generic as this can suffer the same fate. A CSS garden quickly becomes a jungle.
I recently had to add support for multiple themes to a product. The product is going to be OEM'd and needs to represent different companies' brands. The product had a lot of CSS that had become overgrown. In this post, I am going to describe the changes I made to the CSS infrastructure to support multiple themes and to also help the development team better maintain the CSS rules. And even if a project only needs a single theme, I think the following infrastructure is still beneficial to a project.
File Structure
themes
common
images
layout
header.css
pages
account.css
widgets
dialog.css
button.css
common.css
oemA
images
layout
header.css
pages
account.css
widgets
dialog.css
button.css
oemA.css
oemB
images
layout
header.css
pages
account.css
widgets
dialog.css
button.css
oemB.css
The common theme contains general layout rules that will be used for all themes. There are no hard and fast rules for what belongs
in the common theme, but it should be anything that should be used in all OEM themes, including heights, widths, paddings, and margins. Generally the common theme wouldn't contain any color definitions. However, with the work I just recently completed, there was part of the application that used a grey color and did not change based on the OEM, so I put it in the common theme. Again, this can be more of an art and you should do what feels right for you. You can always move things later.
The oemA and oemB directories contain css rules for particular OEMs. Only one of these themes should be used at a time in combination with the common theme. The css rules in these files are specific to the OEM and are generally the color scheme. The product I was recently working on is a business application and we weren't looking to make any major changes to the UI. The colors and in some cases the fonts were changed.
This is a good place to talk about CSS preprocessors. SASS and LESS are two examples of such preprocessors. Preprocessors allow you to define variables (colors, borders, etc) and then use the variables throughout the stylesheets. At first glance, it would appear that these preprocessors would eliminate the need to have oemA and oemB defined. One could just have the a single set of CSS rules defined and apply a different set of variables for each OEM. However, it was my experience that the colors don't translate one for one across themes. For example, in the original theme there were tabs that used the primary color of the theme. When implementing an additional theme, the tabs looked better using one of the secondary colors. It is my opinion that the CSS preprocessor provides value when used within a theme, but should not be used to provide variables across themes.
In the file structure above, the images directory contains images for the particular theme. I don't believe it is a good practice to reference images in a different theme. However, one exception is referencing images in the common theme. The common theme could contain a set of icons and for a particular OEM theme and you might want to override an image with one of those icons. Again, there are no hard and fast rules; this is an art.
The layout directory contains files that define CSS rules for the framework of the web application. These include things like headers, footers and sidebars. The pages directory is for specific page layouts within the application. The widgets directory is for widgets that are used throughout the application. Buttons and dialogs are two examples of such widgets.
The CSS
.themeName .pageOrWidget .theSpecificCssRule
What makes this concept work is nesting CSS selectors. Each CSS rule declaration should begin with the theme that it belongs to. The theme name should be added to the CSS of the body element and by doing so, you now control whether or not a theme is applied to the page. This is how Dojo applies themes.
Each CSS rule declaration should further restrict its use by specifying the page or widget that it is being used for as the second CSS selector. Each combination of the first two CSS selectors should equate to its own file in the file structure above. Any CSS selectors after the first two are specific to the need of the rule.
The Account Page Example
<body class="common oemB">
<div class="logo"></div>
<div class="accountPage">
<div class="button">
<span class="buttonText">OK</span>
</div>
<div class="accountSummary">...</div>
</div>
</body>
/* common/widgets/button.css */
.common .button .buttonText {
font-size: 1.25em;
}
/* oemA/widgets/button.css */
.oemA .button .buttonText {
color: red;
}
/* oemB/widgets/button.css */
.oemB .button .buttonText {
color: green;
}
Looking at this example, I would expect the logo to be defined in header.css, the button and buttonText to be defined in the button.css and accountSummary in the account.css
Compressing CSS
While defining CSS rules across multiple files is good for maintenance, it's horrible for production. So as part of the build process, the CSS files should be compressed and combined into fewer. I generally make one file per theme and the server side code that emits the html knows what stylesheets to include and what css classes to add to the body element.
The General CSS Declaration
.color { color: red; }
<div class="button">
<span class="red">OK</span>
</div>
Avoid the general approach. What happens if we want to make the color blue? We have to modify the code that creates the widget to apply the blue class. What about multiple themes that each use a distinct color? The widget now needs to have knowledge of the current theme. The purpose of CSS is to take this responsibility away from the widget code. By using the general approach, the widget is again responsible for specific styling logic.
The widget code should be thought of as defining the theme "interface" using CSS classes. The CSS rules are the implementations of the "interface". The class names that make up the "interface" should describe their purpose (.buttonText) but not a specific implementation (.red).
CSS Grouping
.oemA .button .buttonText,
.oemA .button .buttonText.important,
.oemA .anotherWidget .widgetText {
color: red;
}
CSS grouping provides the ability to not repeat yourself. While we as developers always try to adhere to the DRY principle, it has been my experience that overusing CSS grouping leads to poor maintainability.
Using the example above, where would the CSS rule reside in the file structure that we previously defined? You could make a common file beneath the widgets directory for rules like this. The problem with this approach is that over time a lot of widget rules would migrate to the common file and when you need to make modifications to a widget, it will require you to look in multiple files. Worse yet, if this rule lives in the anotherWidget file, looking for all button CSS now requires you to look throughout the jungle, thus defeating the purpose of having a separate file per widget.
/* oemA/widgets/button.css */
.oemA .button .buttonText,
.oemA .button .buttonText.important {
color: @red; /* @red is less syntax */
}
/* oemA/widgets/anotherWidget .css */
.oemA .anotherWidget .widgetText {
color: @red; /* @red is less syntax */
}
I think a better approach is to limit the use of CSS grouping to rules that belong in a single file and use a CSS preprocessor to provide common definition across multiple files within a single theme. This allows for the definition of red to be defined once and allows the CSS rules for a specific widget to live in a single file.
Wrapping Up
I have mentioned this a couple times throughout the post, but it bears repeating - using CSS is very much an art. I have tried to define a set of best practices around CSS that allows CSS to do what it was designed to do and still be maintainable across a large project that over time will inevitably have to be modified and even refactored.