CSS and HTML Encompassing Style System (CHESS) is a set of style and design rules for frontend component-based web development.
The goals of CHESS include:
This document focuses on more than merely aesthetic issues like formatting. It also provides design guidance — styling with a purpose.
Jake Knerr © Ardisia Labs LLC
When developing a naming/design system, one has to decide if the system will emphasize writing CSS classes, classes in HTML, styles, or a combination of these priorities. For example, Tailwind maximizes writing classes directly in HTML but minimizes the creation of custom CSS classes and styling.
<!-- Tailwind example - lots of predefined classes in HTML -->
<input
class="bg-gray-50 border-gray-300 focus:ring-3 focus:ring-blue-300 h-4 w-4 rounded"
/>
BEM maximizes writing custom CSS classes and styles, and BEM also requires writing lots of classes in HTML. BEM avoids specificity conflict but at the cost of creating significant overhead for developers.
<!-- BEM example - lots of custom CSS classes and lots of classes in HTML -->
<figure class="photo">
<img class="photo__img" src="https://github.com/jake-knerr/chess/raw/main/me.jpg" />
<figcaption class="photo__caption">Look at me!</figcaption>
</figure>
/* BEM */
.photo {
}
.photo__img {
}
.photo__caption {
}
CHESS aims for a compromise between these different priorities.
Also, most CSS/HTML systems avoid the CSS cascade entirely. The downsides of avoiding all style cascading are that (1) developers must constantly create names for classes (naming is hard), and (2) writing components with repeating elements (like a list) is very clunky because each repeated element requires custom classes. CHESS embraces the idea that the CSS cascade can make life easier and development faster.
Finally, some CSS/HTML systems do not focus on "components." A component is a bundle of styles and HTML nodes that are applied together in a web document. CHESS fully embraces the concept of components and componentizing a web document.
This guide can roughly be broken up into two parts: (1) a series of style rules that are generally aimed at consistency and formatting, and (2) a design system for component-based development.
Feel free to jump ahead to the part of the guide focused on component-based development here.
This guide does not restate formatting rules that Prettier already enforces.
Why Prettier? The Prettier website puts it best: "By far the biggest reason for adopting Prettier is to stop all the on-going debates over styles. It is generally accepted that having a common style guide is valuable for a project and team but getting there is a very painful and unrewarding process. People get very emotional around particular ways of writing code and nobody likes spending time writing and receiving nits.
So why choose the 'Prettier style guide' over any other random style guide? Because Prettier is the only 'style guide' that is fully automatic. Even if Prettier does not format all code 100% the way you'd like, it's worth the "sacrifice" given the unique benefits of Prettier, don't you think?"
Why? The default configuration is reasonable and the easiest to set up.
Linters can help enforce conventions and consistency. However, if you do decide to use a linter, don't let it boss you around because sometimes otherwise good conventions should be violated.
In other words, use lowercase when defining style rules, selectors, declarations, properties, tags, attributes, etc.
Why? Mixing uppercase and lowercase is very dissonant.
/* avoid */
.Btn {
color: RED;
}
/* good */
.btn {
color: red;
}
/*
acceptable - "Jake" is not language-dependent (it has no special meaning to
the CSS language)
*/
.btn::after {
content: "Jake";
}
<!-- avoid -->
<div data-Custom="xyz"></div>
<div style="COLOR: RED"></div>
<!-- good -->
<div data-custom="xyz"></div>
<div style="color: red"></div>
<!--
acceptable - uppercase is allowed in user-specified strings because they are
not language-dependent
-->
<div data-custom="A Lovely Name"></div>
Use descriptive names.
Why? Alphanumeric file names ensure the widest compatibility and is simple.
Why train-case and no underscores? Underlined filenames and links can obscure the underscore, making it easy to miss.
# avoid
Index.html
pageSummary.html
toggleButton.css
toggle_button.css
# good
index.html
page-summary.html
toggle-button.css
toggle-button.css
<meta charset="utf-8">
meta element.Why UTF-8? Because it is compatible with ASCII, compact, and fully supports Unicode.
If an escape sequence is required, leave a comment describing the character.
Why? Escape sequences are less clear than the actual character.
/* discouraged */
.icon {
content: "\20AC";
}
/* preferred */
.icon {
content: "€";
}
Why? The BOM typically does not cause any problems, but it can trigger unexpected issues on older browsers or when parsing string content. Also, the Unicode standard does not recommend using it, "Use of a BOM is neither required nor recommended for UTF-8 ..." Unicode Version 5.0 http://www.unicode.org/versions/Unicode5.0.0/ch02.pdf
In this section, code refers to HTML markup and CSS code.
Why? Comments are an excellent way to break up code sections into discrete parts and provide guidance to other developers in understanding complex or unintuitive code.
Why? Comments complement code; they do not restate code. Since code uses a non-natural language, comments use a natural language to avoid merely repeating the code.
Otherwise, use formal grammar, e.g., proper verb conjugation, subject-verb agreement, etc. Focus on clarity.
This rule only applies if it makes sense with regard to one's natural language. This author intends no English language chauvinism.
Why simplify grammar? Simplifying grammar frees developers from many grammatical debates and decisions. Also, grammatical simplification means that a developer does not have to drift as far from the coding mindset when writing a comment as one would if formal grammar was required. In other words, thinking about formal grammar can throw one's mind out of a coding groove.
/* discouraged */
/*
The CSS rule below adds a red outline to a document HTML node. Also, this
rule is not required for Electron apps.
*/
.red-outline {
}
/* preferred */
/* adds a red outline; not required for electron apps */
.red-outline {
}
<!-- discouraged -->
<!-- The DIV below is used to indicate the author of the article herein. -->
<div>By Jake</div>
<!-- preferred -->
<!-- author -->
<div>By Jake</div>
Use a vocabulary that is understood by the intended audience.
/* fixing ancient ie — web developers know what ie is referring to */
.ie-fix {
}
Also, if one's natural language has different national varieties, pick one to resolve differences between them. For example, if using English, decide to use American English, British English, Canadian English, or any other national variety to resolve differences.
For example, Wikipedia's style guide for English is here: Wikipedia:Manual of Style [English]
Why? A comment enclosed in spaces is much easier to read.
/* spaces are on each side of this comment */
<!-- spaces are on each side of this comment -->
<!––
(HTML) or `/` (CSS).**-->
(HTML) or `/` (CSS).**/*
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
line x
line y
line z
*/
<!--
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
line x
line y
line z
-->
Why? This ensures that the comment is visible regardless of the word-wrapping setting on the reader's text editor.
/* avoid */
/* lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore */
/* good */
/*
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore
*/
<!-- avoid -->
<!--
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore
-->
<!-- good -->
<!--
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore
-->
Also, prefer to not place comments on lines that also contain code. In other words, prefer comments on their own line.
Why immediately above the subject? This convention makes it clear to what code a comment refers to.
Why discourage comments inline with code? Because comments and code mixed within a single line are a distraction that hurts code comprehension.
/* avoid */
/* simple red class */
.red {
color: red;
}
/* good */
/* simple red class */
.css {
color: red;
}
/* discouraged */
.red {
color: /* i like red */ red; /* shows error state */
}
/* preferred */
.red {
/* i like red; shows error state */
color: red;
}
<!-- avoid -->
<!-- author byline -->
<div>
<div class="author">Jake</div>
</div>
<!-- good -->
<!-- author byline -->
<div>
<div class="author">Jake</div>
</div>
Why prefer an empty line? Without a blank line, code becomes cramped.
/* avoid */
.btn {
color: blue;
/* my favorite font */
font: Arial;
}
/* error class */
.error {
color: red;
}
/* good */
.btn {
color: blue;
/* my favorite font */
font: Arial;
}
/* error class */
.error {
color: red;
}
<!-- avoid -->
<div></div>
<!-- below is for buttons -->
<div class="btn"></div>
<!-- good -->
<div></div>
<!-- below is for buttons -->
<div class="btn"></div>
Why? TODO is the most common convention for action comments.
/* TODO add hover transitions */
.btn {
}
<!-- TODO add a logo -->
<div></div>
A reader should be able to comprehend the top-level comment without directly referring to the code.
/*
This file contains the styling for the contact forms.
Author: Jake Knerr
All rights reserved.
*/
.btn {
}
<!--
This file contains the markup for the contact forms.
Author: Jake Knerr
All rights reserved.
-->
<!DOCTYPE html>
Exceptions to valid CSS and HTML are very narrowly prescribed.
Why? Valid code can be expected to work correctly across clients and is easier to maintain.
For example, https://validator.w3.org/ (HTML) and https://jigsaw.w3.org/css-validator/ (CSS).
These are defaults. Other rules in this document may specify more specific attribute ordering.
Why? Alphabetical order provides a predictable and searchable structure.
<!-- discouraged -->
<div disabled data-custom="xyz" alt="xyz"></div>
<!-- preferred -->
<div alt="xyz" data-custom="xyz" disabled></div>
<!-- discouraged -->
<div
class="zeta alpha omega"
style="z-index: 1; color: red; background: black;"
></div>
<!-- preferred -->
<div
class="alpha omega zeta"
style="background: black; color: red; z-index: 1;"
></div>
Why? The valueless option is concise without any loss of clarity.
<!-- avoid -->
<button type="submit" disabled="disabled"></button>
<button type="submit" disabled=""></button>
<button type="submit" disabled="disabled"></button>
<button type="submit" disabled="disabled"></button>
<!-- good -->
<button type="submit" disabled></button>
Why? Because the HTML5 specification and browsers assume JavaScript and CSS for these tags.
<!-- avoid -->
<link href="https://github.com/jake-knerr/chess/blob/main/styles.css" rel="stylesheet" type="text/css" />
<script src="https://github.com/jake-knerr/chess/raw/main/scripts.js" type="text/javascript"></script>
<!-- good -->
<link href="https://github.com/jake-knerr/chess/blob/main/styles.css" rel="stylesheet" />
<script src="https://github.com/jake-knerr/chess/raw/main/scripts.js"></script>
Why? The omission of optional tags can be confusing.
<!-- avoid - head and body omitted -->
<!DOCTYPE html>
<title>Page</title>
<p>Content
<!-- good -->
<!DOCTYPE html>
<head>
<title>Page</title>
</head>
<body>
<p>Content</p>
</body>
</html>
However, one must use HTML entities for characters that possess special meaning in HTML, e.g., <, >, &, etc.
Why? Entities are more verbose and require the developer to memorize what they represent. Also, most entities are unnecessary because documents should be encoded in UTF-8.
<!-- discouraged -->
<div>© ¢ ®</div>
<!-- preferred -->
<div>© ¢ ®</div>
In other words, avoid unquoted attribute values.
Why? Consistency, fewer rules to memorize, greater editor support, and quoted attributes are easier to maintain because values can be edited without worrying about breaking them because quotes are missing.
<!-- avoid -->
<div class=foo>
<!-- good -->
<div class="foo">
Why? There are a lot of default values, and restating them is extremely redundant and verbose.
<!-- avoid -->
<div draggable="auto"></div>
<!-- good -->
<div></div>
However, the most important issue is clarity; the chosen format should be clear.
<!-- discouraged - block elements should be on new lines -->
<header><div>Hello World</div></header>
<!-- preferred -->
<header>
<div>Hello World</div>
</header>
<!-- discouraged - inline element not on the same line as the parent -->
<div>
<span>Hello World</span>
</div>
<!-- preferred -->
<div><span>Hello World</span></div>
Why? HTML5 is more capable than other doctypes, and browser adoption of HTML5 is high.
<!DOCTYPE html>
Why? HTML5's semantic elements add additional context to the document. Also, they can improve search engine results and accessibility.
<!-- preferred -->
<article></article>
Why? Inline styles do not separate the content from its styling. This lack of separation makes styling management more difficult. Also, inline styles bloat the HTML because they can not be reused like style rules defined in style sheets.
<!-- avoid -->
<div style="display: flex; color: red;">
<button>Click Me</button>
</div>
<!-- good -->
<div class="alert-btn">
<button>Click Me</button>
</div>
Why? Complexity should serve a purpose, and extra containers hurt performance.
<!-- avoid - div serves no purpose -->
<header>
<div>
<span>Foo bar</span>
</div>
</header>
<!-- good -->
<header>
<span>Foo bar</span>
</header>
Fallbacks include alt
tags, captions, and any content that will be displayed if the media is not supported by the client.
Why? Fallbacks are necessary for accessibility.
<!-- discouraged -->
<img src="https://github.com/jake-knerr/chess/raw/main/red-balloon.jpg" />
<!-- preferred -->
<img src="https://github.com/jake-knerr/chess/raw/main/red-balloon.jpg" alt="Red Balloon" />
Since one of CHESS's primary motivations is managing CSS specificity, it is recommended that readers possess a firm grasp of how CSS specificity is resolved and calculated.
This is a soft preference.
Why? Sorted properties provide a predictable and searchable structure. Also, the rule is simple and easy to follow. There is no requirement to memorize what attributes should come first, last, or any particular order.
/* discouraged */
.btn {
text-align: center;
box-shadow: 1px #000;
background: red;
}
/* preferred */
.btn {
background: red;
box-shadow: 1px #000;
text-align: center;
}
Why? This way, they are easier to notice, find, and remove as browsers evolve.
/* discouraged */
.btn {
color: red;
display: block;
-webkit-background-clip: text;
-ms-hyphens: auto;
}
/* preferred */
.btn {
-ms-hyphens: auto;
-webkit-background-clip: text;
color: red;
display: block;
}
Why? Shorthand properties are more concise and still very clear.
/* discouraged */
.btn {
padding-bottom: 10px;
padding-left: 10px;
padding-right: 5px;
padding-top: 10px;
}
/* preferred */
.btn {
padding: 10px 5px 10px 10px;
}
Why? They are unnecessary noise.
/* avoid */
.btn {
margin: 0px;
}
/* good */
.btn {
margin: 0;
}
rgba()
format.Why? Hexadecimal notation is the standard and is more concise. However, using hexadecimal to describe an alpha channel value of 0-100 is too tricky for creatures with ten fingers. In other words, using base-16 to describe base-10 numbers (0-100) is incredibly unintuitive. Thus, when an alpha value is needed, use
rgba()
.
/* discouraged */
.btn {
color: rgb(25, 10, 10);
}
/* preferred */
.btn {
color: #190a0a;
}
/* discouraged */
.btn {
color: #00ff00cc;
}
/* preferred */
.btn {
color: rgba(0, 255, 0, 0.8);
}
Why? Three character notation is more concise and still very clear.
/* discouraged */
.btn {
color: #555555;
}
/* preferred */
.btn {
color: #555;
}
/* avoid */
.hugetogglebutton {
}
:root {
--primaryBackground: brown;
}
/* good */
.huge-toggle-button {
}
:root {
--primary-background: brown;
}
/* avoid */
* {
}
body {
}
.btn {
}
/* good */
* {
}
body {
}
.btn {
}
Clarity is king when naming class selectors.
Remember that readers may not be fluent in English. Do not show off your dictionary/thesaurus skills.
What is an intuitive name? It is a name that other developers will recognize and know its meaning.
What is a descriptive name? It is a name that provides adequate detail.
/* discouraged - not intuitive */
.pulchritudinous-button {
}
/* preferred */
.pretty-btn {
}
/* discouraged - not descriptive */
.asset {
}
/* preferred */
.icon {
}
Be phonetic and see if you can drop any letters without a loss of clarity. Think about it from another person's perspective: what might another person imagine the shortened name means?
Think: l (large)
, md (medium)
, sm (small)
, btn (button)
, sum (summary)
, el (element)
, a (link)
, ico (icon)
, txt (text)
.
/* discouraged */
.button-large {
}
/* preferred */
.btn-l {
}
/* discouraged - very large array; readers probably do not know this acronym */
.vla {
}
/* acceptable - readers know NASA */
.nasa {
}
/* acceptable */
.button {
}
/* also acceptable - still clear */
.btn {
}
In other words, prefer using names that describe the purpose of the class instead of how it compares to another state (how it looks).
Why? Semantic names are more clear to the reader and more reusable.
/* discouraged - just states how it looks */
.btn-red {
}
/* preferred - states the purpose of the button */
.btn-error {
}
x
, xx
, xxx
, etc. to provide additional strength to the comparison..btn {
}
/* smaller than default */
.btn-s {
}
/* even smaller */
.btn-xs {
}
/* even smaller */
.btn-xxs {
}
/* larger than default */
.btn-l {
}
/* even larger */
.btn-xl {
}
/* even larger */
.btn-xxl {
}
REM
for font sizes (or anything that should scale with fonts) and use px
for everything else.Sometimes, it may be useful to use REM
for styles that are related to displayed text. For example, the padding around text may be specified in REM
units.
Do not change the browser's default font-size
.
Why? This way, users can change the font size of your document without affecting the layout.
/* avoid */
html,
body {
font-size: 16px;
}
/* good */
html,
body {
font-size: 100%;
}
@layer
at-rule.Why? CHESS already specifies the order of different sections of a document's styles, making
@layer
redundant.
Shadow DOM is a great concept, but implementation considerations argue against its use.
Why? The shadow DOM requires JavaScript, which is unsuitable for many content-focused projects.
I am building a SPA. Why not use the Shadow DOM? Every element using a shadow DOM requires a style tag or a link to a preloaded stylesheet. Parsing a stylesheet for every component has negative performance implications. A solution named 'adopted stylesheets' aims to fix this problem, but Safari has not implemented it.
These principles taken too far will cause madness.
Support is extremely good and it makes code more readable and concise.
/* avoid */
.btn {
}
.btn > div {
}
/* good */
.btn {
> div {
}
}
In other words, different rules for fonts, themes, globals, components, etc. can be added to the document in any order. CHESS makes it so their order does not matter. The only exception is for overrides
, which are rules that must be declared after the rules they are overriding. Overrides
are discussed later in this document.
Also, components can be defined anywhere, but a component's styles must be defined together (more later).
Why? When building an application is can be difficult to completely control the order of style rules. This is especially true when multiple developers are working on the same project. CHESS makes it so the order of rules does not matter.
The more global a rule is, the earlier it should be declared in the document. For example, fonts are typically used by many components, so they should be declared early in the document. However, this is not required.
@font-face
rules early in your stylesheets.Why? Including font definitions towards the top will start the asynchronous download of the font files sooner rather than later. However, with browser performance becoming better and better, this is not as important as it once was.
@font-face {
font-family: Inter var;
src: url("/fonts/Inter.var.woff2") format("woff2");
}
Why? Custom properties work well for creating a default theme, are natively supported, and browser support for them is excellent.
Why? They can help prevent the developer from typing the same styles over and over.
Side effects are styles that when changed force changes in other styles. An example of styles that do not typically cause side effects are "colors", and styles that typically cause side effects are layout styles like "width".
Why are CSS variables so useful for styles without side effects? Without side effects, variables can be adjusted in isolation without requiring other changes, which is a perfect use case for CSS variables and theming.
:root {
background-color: white;
color: black;
}
.dark-theme {
background-color: black;
color: white;
}
Why use the :root selector? This way, variables cascade down for use by the entire document.
/* bright color theme */
:root {
--accent-color: #0000ff;
--code-font: Courier;
--small-border: 2px solid blue;
}
.btn {
border-color: var(--accent-color);
font-family: var(--code-font);
}
More on signals later in this document.
/* default theme */
:root {
--accent-color: red;
}
.__dark:root {
--accent-color: blue;
}
<html class="__dark">
<head></head>
<body></body>
</html>
Why? Other libraries in your document using CSS variables are unlikely to use the same name and the same prefix.
/* no namespace prefixes */
:root {
--primary-color: red;
--secondary-color: #0000ff;
}
/*
namespace prefix b- — risk of name collisions with other libraries has been
reduced
*/
:root {
--b-primary-color: red;
--b-secondary-color: #0000ff;
}
.__(signals-name)
Example: .__dark-theme
Signals do not break encapsulation of style rules because signals are opt-in. Style rules must be designed to use signals. In other words, signals do not directly provide styles.
Why use signals? Because a signal can be added once in the document and trigger style changes anywhere in the document. Sometimes, adding states for all style changes either does not work or is unwieldy. A good example is themes. If every component needed states to accommodate every theme, the document would be unmanageable.
Why are they called signals? Because they signal to other style rules that they should change their styles. Think of the document CSS system being a large observer pattern and signals are events.
/* theme example */
:root {
&.__dark-theme {
/* overriding theme variables */
--background-color: black;
}
}
/* component example */
.btn {
.__dark-theme & {
/* overriding component styles */
color: white;
}
}
<html class="__dark-theme">
<button class="btn"></button>
</html>
This section contains style rules that are applied to elements across the entire document. This includes reset rules.
Global styles are applied to elements across the entire document.
A global is not intended to be applied to specific content. When creating styling intended for specific content, use components, which are detailed later in the section titled "Components."
Globals are useful to prevent declaring the same styles repeatedly.
Why only permit type, or universal selectors in globals? Since globals apply default styling document-wide, more specific styling should easily override them. Type, attribute, and universal selectors have low CSS specificity; class based rules defined later in the document can easily override them.
Why allow type and attribute selectors when other CSS naming schemas do not? To avoid adding a large number of style classes and rules to the document merely to follow the component abstraction. Avoid blindly adhering to abstractions.
/* avoid - specificity too high */
a.btn {
}
/* good */
* {
box-sizing: border-box;
}
a {
text-decoration: underline;
}
div {
box-sizing: border-box;
}
:where
pseudo-class function. The specificity of a global rule must not be greater than a type selector.It is acceptable for a rule to have a specificity of multiple type selectors.
Why? Global rules can be useful to prevent redefining the same styles over and over, but their specificity must remain low so components can predictably override them.
/* avoid */
div.error {
}
input[type="email"] {
}
div#logo {
}
/* acceptable */
div {
&:where(.error) {
}
}
input {
&:where([type="email"]) {
}
}
div {
&:where(#logo) {
}
}
In other words, a reset fixes an inconsistent default style among browsers/clients.
/* not a reset - body.backgroundColor is not inconsistent among browsers */
body {
background-color: red;
}
/* is a reset - body.margin is inconsistent among browsers */
body {
margin: 0;
}
As long as the specificity is just simple selectors, this is permissible.
Why? This way, the global rules are more easily identified.
/* acceptable */
html,
body {
margin: 0;
/* nested globals */
a {
}
div {
}
}
.(component)
Example: .btn-huge
This section is typically the most substantial portion of an application's styles.
.defining-rule {
/* fragments */
div {
}
.fragment__defining-rule {
> span {
}
}
/* states */
&& {
&.state__defining-rule {
}
> .state-2__defining-rule div {
}
}
/* extensions */
.inner-component {
}
/* signals opt-ins */
.__signal & {
div {
color: red;
}
}
/* media & container queries */
@media (min-width: 640px) {
& {
}
}
}
/* animations */
@keyframes state {
}
Why the name component? CHESS's concept of components is well suited for documents built using a view-component-based design since each view-component object can be coupled with a CHESS component to encapsulate styling.
These types will be described later in this document.
For the component name, the use of gerunds, infinitives, and participles are discouraged because even though they can function as nouns/adjectives in a sentence, they are typically perceived as verbs.
Sometimes the term "defining rule" simply refers to the class selector used by the defining rule.
Why is the component name unique? The component name serves as a namespace to group all of the component's style rules, and its uniqueness helps reduce name collisions with other style rules.
Why prefer nounal names? Components are things, just like nouns. They represent content on the page.
/* defining rule for component */
.btn {
}
The defining rule must be applied to content, even if the defining rule doesn't apply any styling.
/* component - defining rule */
.btn {
}
<!-- component is being applied to the content below -->
<div class="button">Click</div>
In other words, multiple components cannot be applied to the same content.
Why? The combining of components' styling leads to confusion.
.btn {
}
.btn-error {
}
<!-- avoid - multiple components applied -->
<div class="btn btn-error">
<svg></svg>
</div>
<!-- component - form-container -->
<div class="form-container">
<div>Welcome to my form.</div>
<!-- component - email-field -->
<input class="email-field" />
<!-- component - address-field -->
<input class="address-field" />
</div>
A component must keep all of its rules in a single file.
Why? By grouping all of a component's style rules, a developer can more easily locate and understand a component's purpose. Also, the order of rules matters and keeping all rules together keeps the order clear and accessible.
For example, instead of red-button
or user-name-input
, consider button-red
or input-user-name
.
Why? Related components will appear together when sorted alphabetically, making it much easier to scan related components in a file tree or a stylesheet.
.btn-huge /* instead of huge-btn */ {
}
.btn-lg /* instead of lg-btn */ {
}
.btn-sm /* instead of sm-btn */ {
}
However, it still must be applied to HTML content.
Why apply a non-existent CSS rule? Even if the defining rule does not exist in CSS, it still functions as a target for selectors used by other component rules.
/* component "btn" - no defining rule */
.btn {
/* fragment */
a {
}
}
<!-- defining rule still applied - necessary to target the `a` fragment -->
<div class="btn">
<a></a>
</div>
If you find yourself using more than a single class selector per line, nest the rule and use the &
selector to target the parent class.
There are exceptions to this rule, but they are rare.
/* avoid - multiple class selectors in a single aggregate selector */
.btn .btn__icon {
}
/* good */
.btn {
.btn__icon {
}
}
Namespacing via a prefix is particularly useful for component libraries since the styles in a library may be used across large numbers of documents.
/* no namespace prefix */
.btn {
}
/* namespace prefix b- — risk of style collisions have been reduced */
.b-button {
}
Why? Putting each component in a separate file is an excellent way to emphasize how components are isolated and decoupled.
Fragments are rules that style a component's child content.
Prefer to use the simplest selectors possible that will not overmatch.
Isn't this madness? The prevailing wisdom is that using child combinators leads to specificity problems and unmanageable CSS. In my experience, as long as components are well designed, selecting inner content via type simple selectors, combinators, and pseudo-classes results in far less work, code, and complexity. The risk of overmatching is real. But, since CSS gives us a huge array of selectors, we can use them to be very specific as to what we select. Instead of ignoring all of the tools CSS gives us, we should embrace and use them.
<div class="btn">
<span><svg></svg></span>
</div>
.btn {
}
.btn {
/* fragment - type selector and combinator to target the svg */
svg: first-of-type;
}
Being very precise with type selectors, combinators, and pseudo-classes can negate overmatching.
<table class="footer">
<tbody>
<tr>
<td>Target</td>
<td></td>
</tr>
</tbody>
</table>
.footer {
}
.footer {
/* fragment - very specific to match exactly - uses a single class selector */
> tbody {
> {
tr > {
td:first-of-type {
}
}
}
}
}
There does not need to be a mapping between a fragment rule and a single document element.
<table class="footer">
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
.footer {
}
.footer {
/* fragment - matches five elements */
> tbody {
> tr {
> td {
}
}
}
}
Such fragments are called "named fragments."
Why use a named fragment? Sometimes, it may not be possible or convenient to select sub-structure using child selectors. A named fragment could ease
querySelector
difficulties.
<button class="button-fancy">
<span class="icon__button-fancy"><svg></svg></span>
</button>
.button-fancy {
/* named fragment */
.icon__button-fancy {
}
}
In this way, a named fragment is a type of sub-component.
Why? It can be very convenient.
<button class="button-fancy">
<span class="icon__button-fancy"><svg></svg></span>
</button>
.button-fancy {
.icon__button-fancy {
/* acceptable */
> svg {
}
}
}
.btn {
/* fragment - using a pseudo-element selector */
&::before {
}
/* fragment - using a pseudo-element selector */
> div::after {
}
/* fragment - using a pseudo-element selector */
table a::first-letter {
}
}
.btn {
/* avoid - fragments defined out of order */
span:last-child {
}
span:first-child {
}
}
/* good - fragments defined in the same order they appear in the html */
.btn {
span:first-child {
}
span:last-child {
}
}
<div class="button">
<span></span>
<span>Click Me</span>
</div>
In other words, the existence of a fragment does not mean it must be applied to document content. Fragments may be created to accommodate different content structure scenarios for a component.
/* component */
.btn {
}
/* fragment - optional; only applied if the button has an icon */
.btn {
svg {
}
}
<!--
notice that the fragment was not applied because the button does not have
an icon
-->
<div class="button">
<svg></svg>
</div>
In other words, applying multiple fragments to the same content is discouraged.
Why? The composition of fragments leads to confusion and specificity issues.
<div class="btn">
<svg></svg>
</div>
/* discouraged - multiple fragments will target the same content */
.btn svg {
svg {
}
& > * {
}
}
Why? Smaller components are easier to comprehend.
This section describes the portion of a component's style rules that are states.
.(state)--(component)
Example: .error--btn
:hover
or :has
) are considered state rules.States can be applied to any content in a component. This includes content already targeted by another rule.
Prefer to use the simplest selectors possible that will not overmatch.
Why prefer verbs and adjectives for state identifiers? States are actions or modifiers, just like verbs and adjectives. They represent change on the page.
.btn {
&& {
/* state */
&.selected--btn {
}
/* state */
&&:focus {
}
}
}
In other more simple terms, a state is for anything that may vary either when created or in realtime due to user interaction.
They should not be used for default styling.
If a rule is dynamic and applied indeterminately — it could be due to user interaction like a click — then a state is appropriate.
Also, if a style rule can vary based on the content's initial configuration, then a state is appropriate.
.btn {
&& {
/*
state - added or removed based on whether the button is selected
(changing content)
*/
&.selected--btn {
}
}
}
.user {
&& {
/*
state - added or removed based on whether the user is banned
(configuration)
*/
&.banned--user {
}
}
}
<div class="btn selected--btn">Click to select</div>
Why? An additional class does not help target the content and creates complexity.
/* component */
.btn {
}
/* avoid */
.btn {
&& {
&.focus--btn:focus {
}
}
}
/* good - states using pseudo-classes do not require a unique class name */
.btn {
&& {
&:focus {
}
}
}
Only apply the state directly to inner child content if necessary.
Why prefer to apply to the top-level node? It is simpler. All a developer needs to know is that to use the state, apply it to the top-level node.
Example - Applying the state alongside the defining rule and targeting inner content with selectors.
.btn {
&& {
&.selected--btn svg {
}
}
}
<button class="btn selected--btn">
<svg></svg>
</button>
Example - Applying the state directly to the child content. Necessary here because there are an indeterminate number of list elements.
.btn {
&& {
& > .selected-item--list {
}
}
}
<ul class="list">
<li></li>
<li class="selected-item"></li>
<li></li>
</ul>
Example - State rule is using two class selectors, which is fine since it is the simplest way to target the fragment with the state applied to the top-level HTML element.
.btn {
/* fragment */
.icon__btn {
}
/* state */
&& {
&.selected--btn {
> .icon__btn {
}
}
}
}
<button class="btn selected--btn">
<span class="icon__btn"></span>
</button>
&
selector.Why? This makes the state's purpose more clear.
/* discouraged */
.btn {
&& {
.selected--btn {
}
}
}
/* preferable - explicitly put the & */
.btn {
&& {
& .selected--btn {
}
}
}
Specificity problems when using multiple states? Consider creating a new state instead.
Even if a fragment has a state applied directly to it, the state should be defined in the state section of the component.
Why? Since states can modify fragments or target unstyled content, it can be tricky to determine where to locate them. For simplicity, put all states in the states portion of the component styling.
/* avoid */
.btn {
/* state nested within fragment */
svg {
&& {
:focus {
}
}
}
}
/* good */
.btn {
svg {
}
&& {
svg:focus {
}
}
}
&&
block.Why? This ensures the necessary specificity and provides an indented block that makes state code more readable.
/* avoid */
.btn {
&&.selected--btn {
}
}
/* avoid */
.btn {
.selected--btn {
}
}
/* good */
.btn {
&& {
/* state */
&.selected--btn {
}
}
}
/* component */
.btn {
&& {
.selected--btn {
}
.error--btn {
}
}
}
<!--
discouraged - states listed before defining rule and selected--btn listed before
error--btn
-->
<div class="selected--btn error--btn btn"></div>
<!-- preferred -->
<div class="btn error--btn selected--btn"></div>
.(composite-component)-(composed-component)
Example: .toggle-btn.btn
Composites can add new rules or override existing composed component rules.
See the examples below.
Why use composites? When creating a composite prevents defining significant amounts of redundant styling. Do not abuse the concept for the same reasons we avoid treacherous class hierarchies when programming.
Wht not just use states instead? Composites are defined externally from the super component in a new file. Being in a new file can be useful when creating a new Javascript-based component, creating new components based on a library, or when the new component is changed so much that it is a fundamentally different component. Otherwise, if building a component from scratch, prefer states.
/* composed component */
.btn {
& > span:first-child {
}
.icon__btn {
}
.hover--btn {
}
}
/* composite component */
.fancy-btn.btn {
/* overriding fragment */
& > span:first-child {
}
/* overriding named fragment */
& .icon__btn {
}
/* new composite fragment rule - not overriding */
&::before {
}
/* overriding state */
&& {
&.hover--btn {
}
}
}
<!-- apply composite and composed classes -->
<button class="fancy-btn btn"><span></span></button>
/* avoid */
.btn.fancy-btn {
}
/* good - composite class selector before composed component's defining rule */
.fancy-btn.btn {
}
This section concerns rules where outer components style inner components.
Create an extension by using the parent component's defining rule to target a child component's defining rule. Extensions can only target an child/inner component's defining rule.
Such rules will typically have two class selectors.
Why use extensions? A common use case is the parent component supplying layout styles to inner components, or making small styling adjustments.
/* component */
.outer {
}
/* component */
.inner {
}
/* extension */
.outer {
.inner {
}
}
<div class="outer">
<div class="inner"></div>
</div>
<div class="parent">
<span></span>
<div class="child"></div>
</div>
/* avoid - extension should be below the fragment and states */
.parent {
.child {
}
> span {
}
&& {
}
}
/* good */
.parent {
> span {
}
&& {
}
.child {
}
}
Extensions may only target the defining rule for nested components.
Why? Breaking a component's encapsulation leads to nondeterministic specificity and styling.
<div class="parent">
<span></span>
<div class="child">
<span></span>
</div>
</div>
/* avoid - fragment overmatches span in the child component */
.parent {
span {
}
}
/* good */
.parent {
> span {
}
}
Why? This ensures the necessary specificity.
.toggle-button.btn {
}
/* avoid */
.parent {
> .toggle-button {
}
}
/* good - included the full defining rule for the composite */
.parent {
> .toggle-button.btn {
}
}
.btn {
> span {
}
&& {
&.selected--btn {
}
}
.inner-component {
}
.__dark-theme & {
&& {
&.selected--btn {
}
}
}
}
Why? Media queries are used to change styles based on the device's screen size. By defining them at the bottom of a component's styles, it is easier to see how the component's styles change based on the screen size.
min-width:
order.Logical break-points are 640px
768px
1024px
1280px
and 1440px
.
Why mobile-first? Writing break-points in ascending order is more intuitive than descending order.
.btn {
&& {
&:focus {
}
}
/* after all component rules define break-points */
/* lowest resolution break-point */
@media (min-width: 640px) {
.btn {
}
.btn:focus {
}
}
@media (min-width: 768px) {
.btn {
}
.btn:focus {
}
}
@media (min-width: 1024px) {
.btn {
}
.btn:focus {
}
}
/* ascending order */
@media (min-width: 1280px) {
.btn {
}
.btn:focus {
}
}
}
Why? This ensures that the specificity of the media query rule will be greater than the rule it is overriding.
.btn {
div {
> div {
> div:first-of-type {
}
}
}
@media (min-width: 1024px) {
/* write the rule being overridden exactly as above */
.btn {
div {
> div {
> div:first-of-type {
}
}
}
}
}
}
Detailed documentation of a component's structure and usage is typically not worth it. Document when something is unclear.
Example - Documentation is useful here because the pseudo-element effect is not obvious.
.btn {
/* ripple effect */
&& {
.btn:before {
}
}
}
Display structure comments above the component's defining rule.
/*
<ul list state--list> # list component
<button? fragment__> # optional named fragment
<div*> # 0 or more divs
<button state-two--list>
*/
.list {
}
/*
<* red> # component that can be any type
<**> # any type repeated 0 or more times
*/
.red {
}
/*
<div|li list-item> # component that can be a div or li
*/
.list-item {
}
/* fancy-btn */
.fancy-btn {
}
/* simple-btn */
.simple-btn {
}
These rules are designed to add styles or change style properties that were defined earlier. They override
earlier-defined rules.
Overrides can change the styling for any rules that came before: globals, components, and everything else are available for overriding. Overrides can also be used to theme an application.
When are overrides appropriate? When one cannot change the HTML or CSS for a document or library, creating new components is not an option, or you are building an SPA that requires it. In such a case, overrides are the only way to change the styling of the document. Library code with hard-coded HTML and CSS is a good use case for when overrides are appropriate.
Why not just use CSS custom properties? While custom properties work to create themes, it can become oppressive, verbose, and inflexible to add a custom property for every style that may need to change. Also, CSS custom properties can not be used to add styles that are not already defined, unlike overrides that can add styles that were not anticipated earlier.
/* component: button */
.btn {
color: red;
}
/* override */
.btn {
color: blue; /* overriding color */
text-align: center; /* overrides can also add new styles like text-align */
}
These are the only rules where order matters.
/* default variables */
:root {
--button-color: red;
}
.btn {
color: var(--button-color);
}
/* overrides */
/* new blue sub-theme */
:root {
--button-color: blue;
}
Note that @keyframes
rules cannot be defined nested within a component. They are defined after the component that uses them.
Many rules that use animations will be component states.
/* discouraged */
@keyframes spinning {
}
.btn {
&& {
&.spinning--btn {
animation-name: spinning;
}
}
}
/*
preferred - animation has the same name and defined after the class that uses
it
*/
.btn {
&& {
&.spinning--btn {
animation-name: spinning--btn;
}
}
}
@keyframes spinning--btn {
}
Is CHESS flexible with regards to its rules?
Yes, be as flexible as necessary. An inflexible approach towards CSS is doomed to fail.
What about id selectors, e.g.,
#image
?
Avoid id selectors for styling because their specificity is so high that CHESS's class-selector-based methodology falls apart.
How does one solve CSS specificity problems?
If in a pinch, an easy way to raise CSS specificity is by repeating class selectors.
/* component */
.btn {
}
/* simple technique to increase specificity - repeat class selector */
.btn.btn {
}
.btn.focused--btn {
}
What about selector performance?
Do not concern yourself with selector performance. The runtime difference between optimized and unoptimized CSS is nearly always inconsequential.
Where are the utility rules?
Inclusion of utility rules would impose more strict rule ordering, which CHESS proscribes. Instead of utilities, use theming rules.
Prominent differences between CHESS and BEM:
Initially, CHESS used the BEM terms block and element. Ultimately, CHESS dropped these terms because they were confusing since block and element already have special meaning in CSS, HTML, and JavaScript. See block (HTML, CSS, JavaScript) and element (HTML).
Tailwind is a utility-first paradigm that eschews component-based design.
I like Tailwind, and I understand the motivation. However, I prefer thinking of documents as being composed of composable blocks rather than building up structure with loads of small classes — especially for SPA projects.
Perhaps the single most crucial point to take from any style guide is to be consistent. When an abrupt style change occurs, it can cause a cognitive break that throws a developer out of their groove. Try to be consistent!
I hope you found this guide to be helpful. Even if you do not agree with a single statement contained herein, I am willing to bet it got the juices flowing.
Thanks for reading!
- Jake Knerr