Front-end guidelines
Table of contents
Browser support
As an organisation, we support:
- MacOS: latest versions of Edge, Safari, Chrome and Firefox
- Windows: latest versions of Edge, Chrome and Firefox
- iOS: latest version of Safari
- Android: latest version of Chrome
CSS
CSS requirements
It's important for each component to avoid style inheritance issues. When in doubt, always include CSS properties that could potentially be inherited from other components or global styles (such as text colour, font weight and leading).
Relative units
CSS supports many different types of unit values. The basic unit of measurement is usually pixels, which is a fixed value. Most other units are relative units: in other words, they are relative to something else, such as a sibling or parent selector, the browser root font size, a device screen, or a percentage of a container.
All of our CSS is written using relative units. This ensures that values remain proportional: for example, if a container is scaled, its contents are scaled with it instead of remaining fixed at a pixel value. We use rem
for all values as most of our work is done in components, which should be modular and avoid inheritance.
1rem (16px)
1.5rem (24px)
2rem (32px)
When defining our rem
values, it's important to have the value as precise as possible, within four decimal points. Browsers interpret rem
values differently: an imprecise number can result in a border appearing thicker or thinner in different browsers, for example.
.block-name {
/* 1px border */
border: 0.0625rem solid transparent;
}
To calculate this number, take the intended value and divide by 16.
Fluid layouts
Where appropriate keep the component to its natural width in order to pass the layout responsibility up to the parent component. This will allow parent components to control responsive design. As for exceptions, consider setting a max-width on text within a component to ensure good readability.
Specificity
Avoid specificity! Write classes to be modular and independent so that they are not at risk of inheriting parent declarations or modifiers.
- Use clear, unique class names that aren’t likely to be used elsewhere
- Avoid overrides by avoiding nested selectors — with each level of nesting, the selector specificity increases
For example, to modify the margin of this section subheadline, every parent in the declaration would have to be included to have a clean line of inheritance.
/* this usage... */
.layout-class-a {
.layout-class-b {
.section-headline {
.section-subheadline {
margin: 1rem 0;
}
}
}
}
/* Means we have to do this to override it: */
.layout-class-a {
.layout-class-b {
.section-headline {
.section-subheadline {
margin-left: 0.5rem;
}
}
}
}
/* Or: */
.section-subheadline {
.layout-class-a .layout-class-b .section-headline & {
margin-left: 0.5rem;
}
}
PostCSS
Front-end development workflows often include preprocessors, such as Sass or Less, to allow developers to write better object-oriented CSS and use advanced programming functions.
By focusing our browser support on the latest versions, we can use many modern CSS features and achieve a similar same result. Browsers now widely support features such as custom properties and functions.
Where we cannot yet use features previously covered by preprocessors, we can use PostCSS plugins.
Example PostCSS plugins and their purpose:
postcss-import
: allow @import statements in CSS filespostcss-nested
: allow nested CSS using syntax similar to Sasspostcss-custom-media
: allow custom media query statements
CSS linting
CSS is a presentational language, which means that browsers often take declarations as suggestions rather than commands. This allows for a lot of flexibility in writing CSS, but it does mean that mistakes will not return an error, and incomplete style blocks could cascade across the published document.
Linting is a tool to check for mistakes at source. A linter takes a set of rules declared in a configuration file and checks that a CSS file meets the formatting criteria.
Our projects use stylelint, a popular CSS linter. It is available on the command line, and in popular text editors and build tools. Instead of writing all of our own linting rules, we import common configurations maintained by the developer community, then add a few modifications.
Examples of linting rules (from AirBnb):
- Use soft tabs (2 spaces) for indentation
- Prefer dashes over camelCasing in class names
- Do not use ID selectors
- When using multiple selectors in a rule declaration, give each selector its own line
- Put a space before the opening brace { in rule declarations
- In properties, put a space after, but not before, the : character
- Put closing braces } of rule declarations on a new line
- Put blank lines between rule declarations
Our recommended linting rules:
- stylelint
- stylelint-config-recommended-scss
- stylelint-config-standard
- stylelint-order
- stylelint-scss
- @namics/stylelint-bem
Custom rules:
color-hex-case: upper
: Use uppercase for HEX colorsat-rule-empty-line-before: always, except: blockless-after-blockless, inside-block
: Enforce empty line before @ rule except in some casesdeclaration-empty-line-before: never
: Disallow an empty line before declarationsrule-empty-line-before: always, except: inside-block, ignore: after-comment
: Enforce empty line before rule except in some casesorder/order: custom-properties, dollar-properties, declarations, rules, etc
: Set of rules to enforce ordering of propertiesorder/properties-alphabetical-order
: Enforce alphabetical ordercolor-hex-case: upper
: Use uppercase for HEX colorsplugin/stylelint-bem-namics
: Linter to enforce BEM naming conventionscss/at-extend-no-missing-placeholder
: Disallow @ extends with a missing placeholder
The latest stylelint configuration is available in The Economist Design System.
Colour functions
Our colour palettes are defined in CSS using custom properties. Each palette includes custom properties in HEX and HSL formats. For general use, we tend to use HEX values as there is a marginal performance increase and less CSS output. However, both are valid for general use. HSL (Hue, Saturation, Luminosity) gives us fine control over color adjustments and is often used by our designers in their workflow.
When changing alpha values, use the HSL custom properties. While the hsl()
function is interpreted correctly as an alias for both hsl()
and hsla()
in most browsers, Microsoft Edge has issues with the alias and custom properties, so we use the full function name. The same applies for the alpha value: Edge does not reliably recognise percentage values, so use decimal.
.block-name {
background: var(--ds-color-chicago-30);
}
.block-name {
background: hsla(var(--ds-color-chicago-30), 0.25);
}
CSS features
Our projects and design system are built with modern browsers in mind, and use well-supported but often nascent CSS features. These features are powerful additions to our toolset, and provide us with the flexibility to create modern, dynamic layouts while encouraging DRY principles.
Our codebase uses flexbox, grid and custom properties.
By utilising PostCSS, our codebase also encourages the use of upcoming CSS features: custom media queries, nested modules and import declarations. PostCSS adds support using the syntax most likely to be adopted by the web standards community and browsers, so that when the features are broadly supported, less work is needed to adopt them.
BEM
Class names are written using the BEM syntax, a naming convention that helps us organise our styles in a way that gives clarity to the relationship between our HTML and CSS. BEM stands for Block, Element, Modifier.
.block-name {
}
.block-name__element {
}
.block-name--modifier {
}
Block styles
Block styles are modular and independent of other elements. They are reusable and often mirror a JavaScript component. A block style can receive a modifier.
HTML:
<button class="button"></button>
CSS:
.button {
}
Block element styles
Block element styles are semantically tied to their parent and depend on them for placement. A block element style can also receive a modifier, but cannot be a descendent of a modified block style.
/* Valid */
.block {
}
.block--modifier {
}
.block__element {
}
.block__element--modifier {
}
/* Invalid */
.block--modifier__element {
}
.block--modifier__element--modifier {
}
Block elements cannot have block elements of their own: instead of block__element__element
, either create a new block style with a descendent (block1__element
, block2__element
), or create a sibling block element (block__element1
, block__element2
).
HTML:
<article class="article article--briefing">
<header class="article__header">
<h1 class="article__headline article__headline--heavy"></h1>
</header>
<aside class="article__aside">
<p class="article__byline"></p>
</aside>
</article>
CSS:
.article {
/* block style */
}
.article--briefing {
/* block style with modifier */
}
.article__header {
/* block element style */
}
.article__headline--heavy {
/* block element style with modifier */
}
Hyphens:
Single hyphens are a great way of avoiding nested block elements while maintaining a relationship with nearby block elements.
HTML:
<article class="article">
<div class="article__section">
<strong class="article__section-headline"></strong>
</div>
</article>
CSS:
.article__section {
}
.article__section-headline {
}
Block modifier styles
For each variation of a block style or block element style, create a modifier. If either has multiple modifiers, each modifier is given a class. Modifiers are not chained together in CSS.
HTML:
<button class="button button--inverse button--secondary"></button>
CSS:
.button {
// block style
}
.button--inverse {
// block style with modifier
}
.button--secondary {
// block style with modifier
}
Formatting
Ordering classes
Order class names as they appear in the application, and following the relationship from parent to child. When layout and content classes share the same CSS file, include layout classes first.
Within media queries, follow the same order as the first declarations. This improves readability and keeps inheritance clear.
.block-name-a {
padding: 1rem 1.5rem;
}
.block-name-b {
padding: 1rem;
}
@media only screen and (min-width: 60rem) {
.block-name-a {
padding-right: 2rem;
}
.block-name-b {
padding-left: 0.5rem;
}
}
Declaring properties
On first declaration, it's important that all property values are defined. This is especially important for margins, padding and borders. This avoids any inherited styles and sets a baseline for modifications made later within media queries.
When setting further modifications, include the individual property values changed. Only include all four values if all four values are modified.
.block-name {
border: 0.0625rem solid var(--ds-color-london-85);
margin: 0;
padding: 1rem 1.5rem;
}
@media only screen and (min-width: 60rem) {
.block-name {
border-color: var(--ds-color-london-5);
margin-bottom: 1.5rem;
padding-left: 2rem;
padding-right: 2rem;
}
}
Shorthand properties
Shorthand properties are useful for writing multiple property values in a concise and readable way. Aim to reduce duplicate values.
/* this: */
.button {
margin: 0 1rem 1.5rem;
padding: 1rem 1.5rem;
}
/* NOT this: */
.button {
margin: 0 1rem 1.5rem 1rem;
padding: 1rem 1.5rem 1rem 1.5rem;
}
Grid shorthand properties
grid-area
is a useful shorthand for writing grid-row-start
, grid-column-start
, grid-row-end
and grid-column-end
. It includes a unique property declaration using slashes. Since we include spaces between other property values, include spaces between the slashes. This is particularly useful as grid-area
properties can contain words, which should naturally have spaces around them.
/* this: */
.button {
grid-area: 2 / 1 / 2 / 4;
}
.button--secondary {
grid-area: 2 / 1 / auto / span 3;
}
/* NOT this: */
.button {
grid-area: 2/1/2/4;
}
.button--secondary {
grid-area: 2/1 / auto/span 3;
}
Nesting classes
Using PostCSS we can write CSS classes with nested rules, similar to Sass and CSS nested modules. Nested rules add the ability to nest one style rule inside another, and reference selectors relative to the rule. This increases modularity and stylesheet maintainability.
In order to keep modularity and portability, do not nest child elements. The exception to this is when there is no choice but to include specificity, or when HTML elements are targeted directly. This should truly be the exception to the rule: nesting classes increases specificity, and elements with complex inheritance are much harder to update.
/* this: */
.block-name {
}
.block-name__element {
}
/* NOT this: */
.block-name {
.block-name__element {
}
}
/* Exception (only if children can not receive a class): */
.ds-list {
li {
}
}
Avoid using the PostCSS ampersand notation for child elements. Writing out the full class name makes it easier to search for, and simpler to view at a glance.
/* this: */
.block-name {
}
.block-name__element {
}
/* NOT this: */
.block-name {
&__element {
}
}
Class relations
Keep classes modular and avoid scoping a block element to a parent or sibling without a modifier. However, in cases where this isn't practical, use the PostCSS ampersand for parent elements:
/* PostCSS */
.article__headline {
.article--briefing & {
}
}
/* Outputs to: */
.article--briefing .article__headline {
}
Pseudo-classes Always nest pseudo-classes and attributes.
.block-name {
&:hover {
}
&:focus {
}
&:active {
}
&:disabled {
}
}
.block-name {
&:first-of-type {
}
&:nth-child(3) {
}
&:last-child {
}
&::after {
}
&::before {
}
}
Imported components
When importing a design system component into a project and modifying it, scope the modifcations to the container in which it is added into.
.layout-section {
.ds-button {
margin-bottom: 1rem;
}
}
Modifiers
To ensure that combined modifiers follow the correct inheritance, it is sometimes useful to nest them. However, only nest modifier classes when combining one or more classes. Avoid nesting a modifier class within a block style definition.
/*
* Button
* Button (inverse)
* Button (secondary)
* Button (inverse, secondary)
*/
.button {
}
.button--inverse {
}
.button--secondary {
&.button--inverse {
}
}
Nesting order
For the examples above, the preferred order following the class declarations are pseudo-elements, modifiers, children.
.block-name {
margin: 1rem 0;
&:hover {
}
&:focus {
}
&:active {
}
&:disabled {
}
&:first-of-type {
}
&:nth-child(3) {
}
&:last-child {
}
&::after {
}
&::before {
}
&.modifier {
}
.parent-modifier & {
}
span {
/* follow same order as parent */
}
}
Multi-line
Always break to a new line when writing declarations and handling secondary classes or child exceptions.
/* this: */
.block-name {
&:hover {
border-bottom-color: #000;
color: #000;
}
}
.block-name {
&.block-name {
}
}
/* NOT this: */
.block-name:hover {
border-bottom-color: #000;
color: #000;
}
.block-name.block-name {
}
CSS modularity
By keeping class names modular and unambiguous, we avoid inheritance conflicts and make finding and updating a class simpler and intuitive.
- Structure CSS files according to component and follow naming of the component
- Avoid splitting class declarations across multiple CSS files, unless there is a clear distinction between layout and content
- Name classes with the broadest purpose in mind, but be mindful of context — while it's tempting to create a
.headline
or.paragraph
class, they aren't likely to fit every situation. Instead, use.section__headline
or.section-headline
, depending on the block-element relationship.
JavaScript
Reusable components
Keep components dumb by pushing logic upwards. For example, parent components that can manage an input
component's state can track and validate user input.
Create reusable molecules by breaking down larger components into smaller ones and considering if a component can be shared. For instance, a shared <Input />
component is used by <TextField />
and <Number />
.
Component props
Instead of passing down all props to a component we prefer to explicitly pass each one. This is to avoid unexpected behaviour, however there are exceptions in cases where traditional elements might have too many options to account for.
// restricted props:
const Paragraph = ({ inverse, children, ...otherProps }) => (
<p inverse={inverse}>{children}</p>
);
// the exception:
const SlimButton = ({
children,
className,
secondary,
inverse,
// accepts: type, disabled, etc.
...otherProps,
}) => (
<button
className={classnames(css['slim-button'], {
[css['slim-button--secondary']]: secondary,
[css['slim-button--inverse']]: inverse,
}, className)}
{...otherProps}>
{children}
</button>
);
NOTE: complexity increases with every new prop added to a component, so consider limiting the number of props for a component and rather creating a new component instead.
// NOT this:
const Paragraph = ({
children,
small,
medium,
large,
extraLarge,
...otherProps,
}) => <p small={small}, medium={medium} large={large} extraLarge={extraLarge}>{children}</p>;
// rather this:
const SmallParagraph = ({ children, ...otherProps }) => ...
const MediumParagraph = ( children, ...otherProps ) => ...
const LargeParagraph = ( children, ...otherProps ) => ...
const ExtraLargeParagraph = ({ children, ...otherProps }) => ...
Accessibility
Interactive pseudo-classes
All interactive HTML elements must include pseudo-classes in their styling. Examples of interactive elements are links, buttons and form fields. Full list of interactive elements.
Which pseudo-classes an element requires will vary (a form field will not have a hover state, for example). Common examples:
:active
:checked
:focus
:hover
:indeterminate
:visited
Focus
It's especially important to include a :focus
pseudo-class on interactive elements as they are a valuable accessibility enhancement.
By default, browsers add their own outline style to focused elements. This is often a heavy outline or blur, though styling differs across browsers.
We can create a consistent experience with our own focus states by visually hiding the outline and adding a border or box-shadow.
/* this */
:focus {
box-shadow: 0 0 0 0.125rem #19D2B9;
color: #142680;
outline: solid transparent;
}
/* NOT this: */
:focus {
outline: none;
}
Documentation and commenting
All written code should be as easy to understand as possible. Favour longer variable names and CSS classes rather than terseness. Longer is better if it's easier to understand. Clear and well-written code is self-documenting.
For example:
/* this: */
const maxNumberOfItemsInCart = 100;
/* NOT this: */
const mNum = 100;
/* this: */
.button--primary {
}
/* NOT this: */
.button--p {
}
In cases where the purpose of a style or line of code isn't initially obvious then provide your reasoning or thinking behind including it.
// accept keystrokes: ↑ ↑ ↓ ↓ ← → ← → B A
const konamiCode = (f, a) => {
document.onkeyup = function(e) {
/113302022928$/.test((a += '' + ((e || self.event).keyCode - 37))) && f();
};
};
// US phone numbers: (XXX) XXX-XXXX (with or without dashes, spaces or brackets)
const phoneRegEx = /^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$/;
.foo {
.third-party-plugin {
/* only way to override inline styles from third-party plugin: */
overflow: hidden !important;
}
}
Webfonts
More subsets for performance
An excellent overview of various web font loading strategies is available on Zach Leatherman's website.
We've optimised the fonts by splitting them into subsets. This ensures that in most cases, on most pages, only a first tier of primary characters is included. This offsets the relatively large number of weights and styles we use given that we frequently use two typefaces in our products.
Due to the large number of subsets we declare in our font-face file, turning the subsets into data URIs within stylesheets would significantly increase the file size and amount of inline styles.
We rely on modern browsers to only load subsets in use for a given page, so methods which preload all fonts / subsets would introduce an unnecessary overhead.
Consuming fonts
Our fonts are available in the design system's common components. For projects not consuming components from the design system, always reference the latest release of font-face.css. This file is always up-to-date and optimal for caching requests.