Writing CSS can be a daunting task, especially if there's already an existing code base that you have to work with. You might be familiar with this process:
- Create a new web page and include the CSS rule set(s) that the rest of your site uses.
- Some styles will be incorrectly applied to your new content, so override them with defaults.
- Some element will be out of alignment, wrong color, or wrong size. Add a class to that element and adjust the margins, sizes, and colors for that class.
- Repeat steps 2 and 3 until the page looks good.
I call it "retrofitting". It's how CSS gets out of control, to the point where everyone is afraid to modify old code, at the risk of breaking some other page or module on the site, and instead developers resort to writing new CSS to override the previous styles. Wouldn't it be great if you could create new web pages without writing hardly any CSS at all?
Setting up
To start, you will need to set up a CSS pre-processor. We use LESS here at SinglePlatform, and it suits our needs perfectly, but if you'd like to use SASS/SCSS, that's up to you. Most web frameworks have plugins that will allow you streamline the pre-processing of your CSS sources. (Tip: generate sourcemaps to make things easier to debug!)
LESS/SASS/SCSS will allow you to have several important features that will make your source code a lot more manageable and compact. The most important ones are variables, selector nesting, and imports. When building a web page, you will only need to include one CSS resource file in a <link>
tag; everything else, like variables and common rule sets, will be included from that file as an @import
.
Namespacing
In order to avoid CSS rules that will unintentionally change styles elsewhere on your site, consider using namespacing. That is, every unique section of your site – whether it's a full page or a page fragment – can live inside a container with a unique id:
<div id="publisher-insights-container">
<div class="section-header">
<h2>Publisher Insights</h2>
...
</div>
</div>
#publisher-insights-container {
.section-header {
margin-bottom: 10px;
}
}
section-header
happens to be a common element, used in many places on our site. However, since we namespaced the custom CSS rule to publisher-insights-container
, it won't affect any other places where it's used.
It helps to indicate the uniqueness of a namespace by assigning it an id
rather than a class
. This will not prevent developers from creating another element with the same id
, but you can run a simple check in your JavaScript console to make sure that doesn't happen:
document.querySelectorAll('#publisher-insights-container').length === 1
Recognizing common layout patterns
If someone asked me to describe what I do for a living, in layman's terms, I would say "recognizing patterns and organizing them into reusable components". I think that this applies to programming in general, and CSS is no exception.
CSS patterns can be organized into rule sets. There are several attributes of your site that are obvious candidates for being a part of the shared rule set: the color scheme, fonts, paragraph line heights and margins, and button styles.
// colors.less
@fontColor: #010101;
@linkColor: #010141;
@backgroundColor: #ffffff;
@errorColor: #bb0000;
// margins.less
@lineHeight: 1.2em;
@paragraphMargin: 20px;
// buttons.less
@buttonPadding: 10px 20px 8px;
button,
.button {
display: inline-block;
text-align: center;
padding: @buttonPadding;
cursor: pointer;
text-decoration: none;
outline: none;
border: 1px solid @linkColor;
color: @linkColor;
background-color: @backgroundColor;
&:hover {
background: @linkColor;
color: @backgroundColor;
}
}
Now we can turn any element into a button just by adding class="button"
, or if the element happens to be a <button>
, then all the work is already done for us.
<button>Click me</button>
But what if we have a custom button with an icon inside – shouldn't that require us to code a custom button style? Nope.
// buttons.less
button,
.button {
&.icon:before {
padding-right: 10px;
}
}
// icons.less
.icon {
&:before {
font-size: 1.5em;
line-height: 0.5em;
vertical-align: middle;
}
&.back:before {
content: "⤺";
}
}
As you can see, the back button was broken down into three reusable patterns: .button .icon
, .icon
, and .back
. The HTML usage syntax will look like this:
<button class="back icon">Go back</button>
In this case, the "back" icon applies to the button, but it doesn't have to. If we want to insert the same icon into a block of text, for instance, we can do so as well:
<div class="helptext">
Click <span class="back icon"></span> to clear the form.
</div>
Moreover, if we need to add another variant of a button with a different icon, all we have to do it define that particular icon:
// icons.less
.icon {
...
&.refresh:before {
content: "⟲";
}
}
All of this code should reside within common rules, because it can be broken down into patterns. Even if these patterns don't get used immediately by other parts of the site, there's a good chance that they will be in the future.
Many of the layout patterns will require more sophisticated rules. For instance, you will likely want all page sections to be separated by a margin. There is no rule that allows you to place a margin "between" two elements, and I've seen many different ways to achieve this, such as adding a class to section headers to denote a margin, or adding a margin to every section header and then offsetting the parent element by a negative margin. However, there's a much simpler way to achieve this, by specifying that every section header except first one will have a margin on top:
.section-header {
&:not(:first-child) {
margin-top: 40px;
}
}
Selectors are arguably the most powerful aspect of CSS. When in doubt about what they can or can't do, you can always consult a reference.
Other layout patterns may be even more complex, like this two-column form, with the right column containing three fields inline:
<form>
<ul class="two-column">
<li>
<label for="id-postcode">Postcode:</label>
<input type="text" name="postcode" id="id-postcode">
</li>
<li>
<span>
<label for="id-latitude">Latitude:</label>
<input type="number" name="latitude" id="id-latitude">
</span>
<span>
<label for="id-longitude">Longitude:</label>
<input type="number" name="longitude" id="id-longitude">
</span>
<span>
<a id="lookup-lat-long"><span class="icon location"></span>Lookup</a>
</span>
</li>
</ul>
</form>
It may be tempting to make a new rule set for this particular form, but there are still several patterns here:
- All input fields should occupy the maximum width available to them.
- If the form is displayed in two columns, these columns should alternate left and right and be separated by a margin.
- If there are inline elements inside a column, they should be separated by a margin.
//forms.less
form {
ul {
$cellPadding: 20px;
&.two-column > li {
width: ~"calc(50% - "@cellPadding/2~")";
&:nth-of-type(odd) {
float: left;
clear: left;
}
&:nth-of-type(even) {
float: right;
clear: right;
}
> span {
display: table-cell;
&:not(:last-child) {
padding-right: @cellPadding;
}
}
}
}
}
Over time, you will find that the rules that only apply to one page – that is, rules that do not follow a common pattern – fit into a short paragraph. For instance, here are the complete contents of location-information.less
from SinglePlatform's Business Portal:
#location-information-container {
#lookup-lat-long {
display: block;
text-align: center;
.icon-location {
text-decoration: none;
font-size: @h3FontSize;
}
}
.delete-column {
text-align: center;
input:checked ~ .icon-trash {
color: @errorColor;
}
}
.parent-business p {
margin: 0;
}
}
There was an initial effort to make the form layout into a reusable set of rules, and the first ten or so forms required some tweaks to those rules, but the benefits far outweigh the effort, since most of the forms that we added after did not require any custom CSS rules at all. For the ones that did, the rule set was consistently under 50 lines.
Breaking the style with layout changes
Sometimes you will find that simply adding an element breaks the common rule set. For example, you may have a hidden element that is required to submit a form.
<div id="publisher-insights-container">
<form action="/users/edit" method="POST">
<input type="hidden" name="csrf-token" value="84e83c03m54143a913"/>
<div class="section-header">
<h2>Active Users</h2>
</div>
...
<div class="section-header">
<h2>Active Users</h2>
</div>
...
</form>
</div>
As a result, the first paragraph now has an unintended top margin. You may be tempted to make an exception for this particular page, or even add a class to every paragraph that should have a top margin. But does this element really belong there? It seems like a tough question, until you ask another: should I let the CSS styles dictate the layout of the page? This idea seems radical, but remember: the common CSS rule set defines how your whole site looks. It is, in a way, a guideline for the site's layout. Once you've made peace with this idea, the answer to the first question becomes obvious: that element does not belong there!
<form action="/users/edit" method="POST">
<input type="hidden" name="csrf-token" value="84e83c03m54143a913"/>
<div id="publisher-insights-container">
<div class="section-header">
<h2>Active Users</h2>
</div>
...
<div class="section-header">
<h2>Active Users</h2>
</div>
...
</div>
</form>
Rather than creating a custom rule set that satisfies a specific scenario, you can change the HTML to satisfy a common layout pattern that dictates the structure of both HTML and CSS. The result will be not only a reduced set of custom CSS rules, but also a cleaner HTML structure.
Refactoring
It's often treated as something that should be done when things start getting out of hand: when maintenance becomes difficult or when there are serious slowdowns. But why wait? Much like cleaning your room, refactoring is most effective when performed on a regular basis.
When working on a specific section of your site, it's easy to lose track of components on other pages. Taking a step back and looking at the big picture will help you identify new patterns that you could organize into common rule sets, and greatly reduce the size and complexity of your code base. Even in the middle of a project, if you find that a page-specific rule set grows above 50 lines, it might be a great time to refactor.
Spending time to find style patterns on your site and organizing them into reusable components may seem like a lot of work at first, but in reality it's a fairly small initial investment. It will quickly become rewarding, when you start building pages that don't require custom CSS. Not only will you save time on creating new web pages, but your entire site will start looking more streamlined, consistent, and professional!
Comments !