Skip to content

Instantly share code, notes, and snippets.

@paceaux
Last active May 19, 2024 11:15
Show Gist options
  • Save paceaux/f31e278613ab29b74a412a7eb5046422 to your computer and use it in GitHub Desktop.
Save paceaux/f31e278613ab29b74a412a7eb5046422 to your computer and use it in GitHub Desktop.
CSS Guidelines

CSS Guidelines

Introduction

Value of Guidelines

These are guidelines for writing stylesheets at (company). (company) may have large teams of front-end and back-end developers responsible for large code bases. As the front-end team grows, (company) acquires new clients, or continues maintaining relationships with existing ones, the quality of a codebase can go down. These guidelines serve to establish a level of quality and to guide developers in how they can maintain that quality.

Flexibility of Guidelines

These guidelines are merely that; guidelines. They are not strict rules or a checklist that can never be changed. There are at least four external forces which require Company to review and/or revise the guidelines:

  • The W3C introduces changes to a CSS specification
  • Acceptance and implementation of a CSS specification within browsers
  • Change in accessibility criteria within the industry
  • Shift in industry best practices

Company should encourage its front-end developers to review and revise the guidelines under several other circumstances as well:

  • Adoption of a CSS preprocessor
  • Updates in the syntax of a preprocessor
  • Introduction of new methodologies
  • Shift in project workflow elsewhere

Goal and Use of Guidelines

The goal of the CSS guidelines is to establish a level of quality and the means to achieve that quality.

Therefore, this document should unify developers in how to:

  • Write transparent and readable CSS
  • keep code maintainable
  • make stylesheets scalable

These guidelines are meant to describe quality, rather than prescribe it. If there is a better way to solve a problem, a developer is encouraged to update the guidelines - not deviate from them.

Organization of Files

It is very important that CSS be written in a structured and organized fashion. This improves onboarding for new developers, and enables the CSS to be scalable for the life of a web application.

Separation of Concerns

The "concerns" of CSS fall in to three general categories:

  • Theme / Skin
    • Typography: font-family; font-weight; line-height; text-decoration; etc
    • Color Palette: color; border-color; background-color; etc
    • Iconography and Images: background-image; font families for icons, etc
    • Default User Interactions: link, form, and button states; transitions; etc
  • Global items and Layout :
    • Layout of page templates
    • Header, footers, navigation
    • Global Responsive design Modules
    • print / alternative stylesheets
  • Content and UI
    • Presentation of content-managed items
    • Presentation of Forms
    • Presentation of Interactive elements: image sliders; forms; calculators; etc

CSS should be organized along these concerns, and separated as necessary along these categories.

Multiple Files

There are a number of benefits in writing CSS is multiple files. At Company, CSS should be written across multiple files for two reasons:

  • Separation of concerns
  • Division of labor

Initially, the CSS should be separated across the three major concerns:

  • Theme & Skin
  • Global and Layout
  • Content and UI

These files may be further split up along a division of labor: Multiple aspects of the presentation; a CSS file per page template,

Subdividing concerns

Styles are expected to be divided into at least three files. The scope of the project and the labor force may require styles to be divided into smaller sections; this is good. CSS should be divided so long as it further separates concerns and optimizes the division of labor amongst the team.

Delivery

Unless otherwise indicated, a minimum of two items should be delivered to the client:

  1. Guidelines document.

    • It is modelled after this one
    • Indicates the architecture and methodology
    • Outlines files and build system
  2. One minified CSS file containing all styles for the site

    • Either a CSS preprocessor, a node.js-based build tool (gulp), or Tridion should be used to concatenate all files into a single file.

Comments

Comments are a critical part of writing clear and transparent styles. CSS, by its nature, relies on markup and even JavaScript for styling elements in the browser. Therefore, it cannot, on its own, describe what is happening and why. Comments are also useful for marking sections of a stylesheet.

High Level Comments

There are cases where an entire section make require an in-depth explanation for how it works and is to be used. For comments that describe an entire section of CSS, use a DocBlock-esque multi-line comment. This should adhere to an 80-column width.

/**
 * The site’s main page-head can have two different states:
 *
 * 1) Regular page-head with no backgrounds or extra treatments; it just
 *    contains the logo and nav.
 * 2) A masthead that has a fluid-height (becoming fixed after a certain point)
 *    which has a large background image, and some supporting text.
 *
 * The regular page-head is incredibly simple, but the masthead version has some
 * slightly intermingled dependency with the wrapper that lives inside it.
 */

Low Level Comments

As a general rule, anything that is not immediately obvious from the code needs a comment.

In more specific terms, any of these scenarios merit a comment in the CSS:

  • Inconsistent vendor prefixing (using only one vendor prefix, instead of all of them)
    • using -ms-transform to target IE10's position:absolute transform bug
    • using -webkit to target only Safari
  • Browser hacks; any method for targeting a specific browser
  • unnecessarily high specificity
    • .someClass[class]
    • #someId.someClass
  • every use of !important or :not()
  • targetting any class name put in by modernizr, or any other feature detection

A low level comment should appear directly above a selector, if it concerns an entire ruleset:

/*Modernizr provides a no-filters class; provides support for IE9 and less*/
.no-filters .article-image {
    filter:progid:DXImageTransform.Microsoft.Blur(3px);
}

A low-level comment should appear directly after the closing semicolon for a property, if it concerns only that property:

.article_title {
    color: #ffeeee\9; /*targets IE6, 7, 8. browser scope changed after delivery*/
    color: rgba(255,238,238, .9);
}

Comments for Bug Fixes

When CSS is written to resolve bugs that surfaced in a production version of the site, and the resolution came from an external site (such as Stack Overflow), include a link to the page in the comment.

/* see: https://dev.opera.com/articles/css-will-change-property/ */
.sidebar {
    will-change: left, top; 
}

In general, code that is copied or refactored from other sources should have a reference to the original source (and proper accreditation or license information, if necessary);

Titling

Every major section of a CSS project must start with a title. A section can be any of the major or minor concerns that have been outlined already. The top of every document should have a section title. Especially when a utility is being used to concatenate files, section titles become a critical tool in debugging a CSS file.

  • The first line of the title should begin with /*===============
  • The second line should contain the title
    • indented one tab (equal to four spaces)
    • prefixed with a hash (#) symbol
    • uppercase
  • the third line should end with ` ===============*/

A title should of a section would then appear like so:

/*===============
    #GLOBAL 
 ===============*/

As we will not be styling on IDs, or writing upper case selectors, this makes the section searchable.

Syntax and Formatting

CSS is expected to literally be written in a standard and unified way. Consistency amongst the writing style will make it easy for developers to transition from one project to the next. Some CSS preprocessors and templating tools may rely on indentation as well; consistent formatting helps reduce compile errors.

  • Indentation is with one tab (which is equal to four spaces)
    • The IDE or text editor should be set to translate tabs into spaces
  • Columns should be no more than 80 characters wide
  • CSS should be multi-line
  • Meaningful use of whitespace

Formatting of a ruleset

Below is a standard formatting of a ruleset:

.foo, .foo-bar,
.baz {
    display: flex;
    flex-direction: column;
    border: 1px solid #333;
}

We describe the formatting of this ruleset:

  • The ruleset has three selectors
    • Similar selectors are on the same line
    • Selectors are separated by a comma, followed by a single space
  • The opening brace { is on the same line as the last selector
  • The ruleset has three declarations
    • each declaration is on a separate line
    • each declaration is indented one tab
    • each declaration is terminated with a semicolon
  • The property name is immediately followed by a colon
  • Exactly one space separates the colon from the value
  • The closing brace } is on its own line

Multi-line CSS

CSS should be written across multiple lines. There are several reasons that CSS should be written in multiple lines:

  • Reduced chance of merge conflicts
  • More reliable diffs, because each line carries one change
  • Improved readability

Even in the case of a single selector, and a single declaration, it is preferrable for this to be multiline.

Therefore, this:

.social-icon-facebook {background-position: -16px 0;}

Should instead be written as this:

.social-icon-facebook {
    background-position: -16px 0;
}

This allows changes to the selector and/or the declaration to be captured in separate diffs.

Nesting

Sass and Stylus are both acceptable CSS preprocessors that can be used for building a web application, and both offer nesting functionality.

Nesting selectors is a dangerous practice. Nesting is encouraged only when it is important to keep rulesets isolated in their scope.

In Sass (and Stylus) nesting can be constructed like this:

.section {
    display: flex;
    flex-direction: column;

    .article {
        display:inherit;
        flex-direction: row;
    }
}

Indentation is done with one tab (which should be equal to four spaces), and there should be a blank line before the nested ruleset.

Favor more specific classnames over nesting class names:

Favor this:

.section {
    display: flex;
    flex-direction: column;
}

.section__article {
    display: inherit;
    flex-direction: row;
}

Over this:

.section {
    display: flex;
    flex-direction: column;

    .article {
        display:inherit;
        flex-direction: row;
    }
}

Nested code has the potential to become increasingly difficult to read as it grows over time.

Parent / Descendant Selector (&)

The CSS author is allowed to use the parent selector (&) which is native to both Sass and Stylus.

When using the parent selector:

  • Use it to group pseudo elements and pseudo classes
  • Place it at the left-most position in the selector
  • Avoid increasingly complex class names
  • Avoid using it to construct a class name
  • Don't nest parent selectors
  • Use meaningful whitespace to improve readability (one line separating properties from the element)
Application with Pseudo Classes

The parent selector is very useful when styling pseudo classes for an element. Group related pseudo-classes on the same line:

a {
    text-decoration: none;
    border-bottom: 1px solid transparent;

    &:hover, &:focus, &:active {
        border-bottom: 1px solid #454545;
    }
}
Application in BEM

If BEM is a methodology that is being used, then it can be tempting to use the parent selector as a base for writing BEM-style block class names, element class names, and modifier class names. Avoid this temptation.

This is not acceptable:

.section {
    display: flex;
    flex-direction: column
    height: 200px;
    padding: 1em 1em;

    &--collapsed {
        padding: 0;
    }

    &__header {
        height: 100px; 
    }
}

With the above code, it would be impossible to search for .section--collapsed or .section__header in the codebase.

Instead, use this:

.section {
    display: flex;
    flex-direction: column
    height: 200px;
    padding: 1em 1em;
}

.section--collapsed {
    padding: 0;
}

.section__header {
    height: 100px; 
}

Alignment

Always align properties to the left. Properties should be indented one tab (equal to four spaces) within the selector.

Alignment by property name:

.foo {
    -webkit-transform: translateX(5em);
    transform: translateX(5em);

}

Whitespace

Whitespace should both consistent and meaningful

  • Use one empty line between rulesets

  • Use three empty lines between sections

    .foo { transform: translateX(5em); }

    .bar { transform: translateX(5em); }

When nesting elements, separate a nested selector from properties with one empty line:

.foo {
    margin: 0 2em;
    transform: translateX(5em);

    .bar {
        transform: translateX(5em);
    }

    .baz {
        padding: 0 2em;
    }
}

Application of Media Queries

There are two methods in which media queries can be applied:

  1. As breakpoint, containing many rulesets
  2. Within a ruleset, containing many breakpoints

There is an appropriate usage of each method.

One breakpoint, many rulesets

When a ruleset is not meant to be extended, it is better to group that ruleset under a breakpoint.

@media screen and (min-width: 380px) {
    .main {
        width: 400px
    }

    .main__header {
        height: 3em;
    }
}

All breakpoints should be placed at the end of a given file. They should be ordered from smallest breakpoint to largest.

One ruleset, many breakpoints

When a ruleset is meant to be extended, it is better to place breakpoints within that ruleset:

.article {
    width: 400px;
    padding: 0 2em;

    @include width-more-than(400px) {
        width: 100%;
    }

    @include width-more-than(401px) {
        padding: 0;
    }

}

.newsArticle {
    @extend .article;
    margin: 0 1em;
}

Selectors

The selector for any given ruleset should

  1. Have the lowest specificty possible,
  2. while still being as specific as possible, and
  3. make as few assumptions about the markup as possible.

Lowest Specificity Possible

After a reset, the baseline styles for the stylesheet should be written using a single element selector. The lowest layer of branding for the site should be applied using selectors that contain only a single element:

h1 {
    font-size: 2em;
}

p {
    font-size: 1em;
    line-height: 1.5;
}

After baseline branding is applied with single elements as the selector, styles should be applied using a single class selector:

.header {
    border-bottom: 1px solid #333;
}
  • The baseline presentation of an element shouldn't have a specificity greater than 0,0,1,0.
  • IDs should not be used for selecting elements

As Specific as Possible

Another way to look at this is to think of "Selector Intent". Think of what you are intending to actually style, and write a selector that targets this.

If the intent is to target the heading text for a section, you might write:

.text {}

However, this targets every .text on the page, but this style only needs to apply to .text when it's inside of the header.

We may also write:

.header .text {}

This assumes that there is a .text{} rule, but that it must be changed or overwritten for its usage inside of .header. If the text inside of the .header doesn't have styles that apply anywhere except .header, then this is too generic.

Instead, be as specific as possible by targetting a class name that applies only to this specific module:

.header__text {}

Unsafe Assumptions About Markup

Styles should make as few assumptions about the markup as possible. It is very important that we keep this in mind because we are writing code that may be generated by a content management system. We cannot assume that content authors will generate content in the same way we expect them to.

  1. Don't assume that an HTML element will never change
  2. Don't assume that the order of HTML elements will never change
  3. Don't assume that elements will be nested in a specific way

Chaining Element name to a Class

Do not write selectors where a class name is chained to an element:

a.button {}
h2.title {}

Instead, write a selector that applies to any element that uses the class name

.button {}
.title {}

This allows for the markup to change without impacting styles.

Chaining Class to a Class

Do not write a selector where two classes are chained together to form a new style:

.button {
    padding: 1em 2em;
}

.collapsed {
    height: 0;
}

.button.collapsed {
    padding: 0;
}

Instead, either write a single selector for applying this unique style:

.button {
    padding: 1em 2em;
}

.collapsed {
    height: 0;
}

.button-collapsed {
    padding: 0;
}

or modify one of the other classes:

.button {
    padding: 1em 2em;
}

.collapsed {
    height: 0;
    padding: 0;
}

This allows for classes to be reusable and produce a predictable result.

Assuming Order of Elements

Avoid using "sibling" selectors.

.panel  + .footer {} /*assumes a footer will always come after panel*/
.panel.visible ~ .footer {} /*assumes the same*/

Instead, try to be as specific as possible:

.panelContainer__footer {}
.panelContainer-hasVisibleFooter {}

Sibling selectors are best-used when targeting exceptions and edge cases. Understand that the sibling selector is more likely to not be used.

Assuming Nesting of Elements

Avoid using elements as general descendants of a class:

.panel h1 {}
.panel p {}

Instead, use a more specific class name:

.panel__title {}

or use a class name whose declarations are identical to an element:

.panel__title .p {}

Generally Recognized As Safe (GRAS) Selectors

Common Attribute Selectors

There are certain attributes that should only applied to certain elements. In those cases, it's perfectly acceptable to chain an attribute to an element:

input[type="text"] {} 
textarea[required] {}
a[href$="pdf"]

In fact, in these cases, lowering the specificity would create more confusion to other developers:

[type="text"] {} /*What, other than input, has the attribute type, and the value text?*/
[href$="pdf"] /*what, other than a, has the attribute href*/

This goes back to the idea that a selector should have as low a specificity as possible, but still be as specific as possible.

Uncommon Attribute Selectors

The attribute selector is extraordinarily powerful; it allows us to select elements in a variety of ways. However, the attribute selector should be avoided most of the time. In cases where we do not have control of the HTML, we may resort to using these selectors. However, each use should be accompanied with a comment, explaining the usage.

Avoid targetting an attribute twice:

.header[class] {} /* the . informs us that it already has a class name. Why arbitrarily raise specificity?*/

Avoid targetting the partial value of an attribute :

[class*="grid"] {} /*Why can't you apply .grid to the element?*/

Do not target an ID as a class:

[id="topnavigation"] {} /*Why can't you apply a class name to it*/

Markup Produced by Rich Text Fields

In a content management system, within a component, it is entirely possible that certain fields are rich-text fields. You should know which markup is produced by a rich text field before you write your code. You should also work with a business analyst or back-end developer to appropriately limit what the rich text field can produce (it is possible to disable images, h1-h6, tables, etc). In these cases, the component template produces a wrapper for this field, and it is acceptable to target element names alone:

.panel__RTF h2 {}
.panel__RTF p {}
.panel__RTF blockquote {}

Unsafe Pseudo-classes

There are some pseudo classes that should be assumed as unsafe by default because they carry side-effects. These are to be avoided unless there is a specific problem that only these can solve and the side-effects are manageable.

:not()

Avoid using :not() because it raises the specificity of matching items unexpectedly, which causes specificity bloat:

Absolutely never pass an argument with higher specificity than the chained item

    .title:not('#mainTitle') {} // every .title that isn't .red has a specificity of 1,1,0

Avoid passing an argument of equal specificity

    .title:not('.red') {} // every .title that isn't .red has a specificity of 0,2,0

If you must, and can, use an item of lower specificity

    .title:not('h2') {} // every .title that isn't h2 has a specificity of 0,1,1

Avoid using it to solve structural markup non-problems

Sometimes :not() is used to avoid writing a second ruleset that applies to fewer elements:

.nav li:not(:last-child) {  // all li except the last one have a specificity of 0,2,0
border-bottom: 1px solid gray;
}

But instead, this raises the specificity of all elements but one. Favor a selector that targets the element that needs modification:

.nav li:last-child {
border-bottom: none
}

:is()

Avoid using :is() for the same reason to avoid :not(): it raises specificity. Even more frustratingly, :is() will use the highest specificity in the list and apply it to all matches:

.links:is(.nav, ol) {} // <div class="nav links"> is 0,2,0 ; <ol class="links"> is ALSO 0,2,0

If you must use :is(), keep all arguments the same specificity

Conventions and Methodologies

Everything that we do, we do for the content. Content should drive how we write semantic markup, and it guides the conventions and methodologies we use in our CSS. There is no one-size-fits-all technique or pattern. So choose approaches that are known in the industry and solve the problems you have now and may have in the future.

Class Prefixing

JavaScript selectors

If a class name is needed solely for the purpose of selecting it with JavaScript, then the class name should be prefixed with js-:

$('.js-accordion')

js- classes should never be used for styling. They should only be used for JavaScript.

** ---- CONSIDER REMOVING ---- **

State selectors

Sometimes, a class name may be needed for state. A state is used to indicate that there is a change to the presentation of the element based on something triggered in JavaScript. When a state needs to be indicated, it should be prefixed with is-. This is because is- indicates a temporary state of being for the element:

$('.js-checkbox').on('click', function () {
    $(this).parent().toggleClass('is-checked');
});

.is-checked {
    border-color: #33ff33;
}

OOCSS

One of the characteristics of maintainable and scalable CSS is the separation of concerns. When CSS separates concerns appropriately, it should be reusable across the team, the current project, and future projects.

CSS is reusable when it is object-oriented. Object-Oriented CSS separates concerns in two ways:

  1. It separates structure (layout) and skin (presentation)
  2. It separates container (section wrapper) and content (component)

OOCSS applied with BEM

An object is any item which can or should be reused. The object does not make assumptions about markup, or where it may be located. It should the same, regardless of where it is located on the page.

Example 1:

Consider an object that represents media. The default state will contain only structural information:

.media {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    align-content: center;
    padding: 40px;
}


.media-neutral {
    border: 1px solid #333;
    background: #fefefe;
}

Single-Responsibility CSS

Single Responsibility CSS is built on the idea that our rulesets should be focused on doing one thing, and only one thing. This concept improves how we can make the "modifiers" to our "objects" more reusable. Single responsibility CSS means that we spend time abstracting rules in meaningful and reusable ways.

Consider the previous example. But instead of one rule, and one modifer, there are five that are applied:

.flexColumn {
    display: flex;
    flex-direction: column;
}

.flexCenter-all {
    justify-content: center;
    align-items: center;
    align-content: center;
}

.PAL {
    padding: 40px;
}

.neutralDarkBorder{
    border: 1px solid #333;
}

.neutralLightBg {
    background: #fefefe;
}

Abstracting sets of Rules

Never abstract into a single isolated rule. Always abstract into a set of rules.

The result of the above abstractions would be something like the following:

.flexColumn {
    display: flex;
    flex-direction: column;
}

.flexRow {
    display: flex;
    flex-direction: row;
}

.flexCenter-jc {
    justify-content: center;
}

.flexCenter-ai {
    align-items: center;
}

.flexCenter-ac {
    align-content: center;
}

.flexCenter-all {
    justify-content: center;
    align-items: center;
    align-content: center;
}

.pal {
    padding: 40px;
}

.pam {
    padding: 20px;
}

.pas {
    padding: 10px;
}

.neutralDarkBorder{
    border: 1px solid #333;
}

.neutralLightBorder: {
    border: 1px solid #fefefe
}

.neutralLightBg {
    background: #fefefe;
}

.neutralDarkBg {
    background: #333;
}

Abstract for all variations

When a rule is abstracted, abstract for all of the variations, not just one set of variables.

In the above example, a single set of rules was created for padding. They were acronyms for Padding All Large, Padding All Medium, Padding All Small. This implies that there are options where padding is applied in only one position. So, proceed to abstract for padding in each position:

.ptl, .pal, .pvl {
    padding-top: 30px;
}

.prl, .pal, .phl {
    padding-right: 30px;
}

.pbl, .pal, .pvl {
    padding-bottom: 30px;
}

.pll, .pal, .phl {
    padding-left: 30px;
}

.ptm, .pam, .pvm {
    padding-top: 15px;
}

.prm, .pam, .phm {
    padding-right: 15px;
}

.pbm, .pam, .pvm {
    padding-bottom: 15px;
}

.plm, .pam, .phm {
    padding-left: 15px;
}

.pts, .pas, .pvs {
    padding-top: 5px;
}

.prs, .pas, .phs {
    padding-right: 5px;
}

.pbs, .pas, .pvs {
    padding-bottom: 5px;
}

.pls, .pas, .phs {
    padding-left: 5px;
}

** ---- CONSIDER REMOVING BEM----** ** ----BEM should come at the end ----**

BEM

The BEM convention is Block, Element, Modifier. In traditional BEM double-underscores and double-hyphens denote elements and modifiers. We follow this practice because it provides a visual indicator that the classes are not meant for reusability.

.article {} /*block*/
.article__header {} /*element*/
.article--small {} /*modifier*/

Application of BEM

BEM is an extremely useful convention when working with a content management system. We will use BEM in order to denote the content model when we create component templates. Ideally, the front-end developer should have completed schema documentation before starting workd. This schema documentation provides the front-end developer with the "block" name and the "element" names that should be used.

If the front-end developer does not have schema documentation, then it is a shared responsibility between the developer and business analysts to establish a "good idea" of what the content model will be so that the developer can write meaningful class names.

The Block

The Block coresponds to a Schema; it is a single piece of content that an editor can create, edit, or remove. When the front-end developer knows the schema for a particular design, the block name should match the schema name. If it is two words, then it should be camelcased:

.insightTeaser {}
The Element

The Element corresponds to a field within the schema. This is an individual field that the content author edits within the content management system. The field is denoted with a single underscore:

.insightTeaser__title {}

When the content management system is Tridion, the content author should be aware of when certain content patterns may be reused in different content types. Schemas in Tridion may contain either links to other components, or embedded schemas. If this is the case, then the content author is encouraged to nest elements:

.insightTeaser_link_title {} /* _title indicates that this is a field within the link schema*/

The Modifier

The modifier corresponds to a specific presentation of the content. When the content management system is Tridion, this corresponds to a component template.

.insightTeaser--type1 {}

BEM with Templates

BEM should be used primarily for applying styles that are specific to a content model. Style the "default" or shared styles between templates under the block. Use the modifier class to style differences between component templates, not override a default state:

This would be a poor application:

.insightTeaser {
    width: 100%;
    height: 100px;

    &__title {
        color: #454545
        font-size: 2em;
    }
    &__image {
        float: left 
    }
}

.insightTeaser-type2 {
    .insightTeaser__image {
        float: right;
    }
}

Instead, this would be better as:

.insightTeaser {
    width: 100%;
    height: 100px;

    &__title {
        color: #454545
        font-size: 2em;
    }
}

.insightTeaser-type1 {
    .insightTeaser_image {
        float: left;
    }
}

.insightTeaser-type2 {
    .insightTeaser_image {
        float: right;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment