Every stylesheet should be easy to read, scan, add to, and collaborate on. Our current system and nomenclature builds on top of components, where CSS files live alongside the component they are styling: component/style.scss
. These files are all imported into the React components' JavaScript sources and then bundled by webpack to the production CSS files.
This is an example of a declaration:
.component__element.is-modifier {}
Two important considerations:
- The
component
fragment matches the folder name of the React component it's providing styles to. - The
.is-modifier
class should never be written on its own outside of the component class. This avoids naming collisions and provides a consistent use of modifiers. - We don't use
#ids
for style purposes.
Take a component called site/index.jsx
that renders a site item on the picker. Imagine we are going to set the color for the site title to #333 and change it to #444 when the site is a Jetpack site. Our Sass file will sit alongside site/index.jsx
and be called site/style.scss
. Its content will be written like:
Good
.site__title {
color: #333;
&.is-jetpack {
color: #444;
}
}
Bad
.site {
.title {
color: #333;
.jetpack {
color: #444;
}
}
}
The modifier classes are always attached to a base element (the wrapper of a component). Since every element will have a direct class to be selected with, we avoid descendant selectors and child selectors. We also avoid indenting selectors in Sass files in favor of single selectors. The main exceptions are precisely the is-modifier
class, and the cases where the context needs to change a given component's accidents.
// Modify 'site__title' for in CurrentSite component's display
.current-site .site__title {
color: #444;
}
The expressiveness of the selector above clearly conveys that there are two separate components involved (CurrentSite and Site). We also keep CSS specificity in check. Avoid using Sass indents for simple selectors — they obfuscate the cascade.
It's important to note that we don't reuse classes, we reuse components.
These practices mean that a component will work anywhere it is included by default, and that the developer won't need to go hunting for the relevant CSS. Any modifications will happen in the context/parents, and will be minimal in nature. Where and how changes will affect the rendering of the app will thus be clearer. (If you edit the button-component's Sass file you know you are editing it for everyone.)
Apart from the above structure, please adhere to these guidelines:
- We use Stylelint to enforce a consistent code style. You can check for style lints by running
yarn lint:css
. Please see IDE setup section for more details. - Follow the WordPress CSS Coding Standards, unless it contradicts something stated in this document.
- Avoid
#
selectors for style purposes. Their specificity becomes troublesome to manage fairly quickly. - Don't use the
!important
declaration. If you think you need to do it, consider fixing the root cause. - Avoid using universal selectors (
*
). - Use hyphens, not underscores or camelCase, when naming things like IDs, classes, variable names, mixins, placeholders. Good:
.site-title
, Bad:.siteTitle
or.site_title
. - The only exception is the
__
syntax to signal the relationship within a component. - Avoid using over-qualified selectors like
div.my-class
.
In order to enable auto formatting of style files, please set up your IDE.
To get things set up in VS Code, do the following:
- Run
yarn install
- Install the official Stylelint extension -
stylelint.vscode-stylelint
- Adjust the following settings in
settings.json
- (Cmd + Shift + P
>Preferences: Open Settings (JSON))
:
{
"stylelint.validate": [ "css", "scss", "sass" ],
"[css][scss][sass]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
// other actions go here
}
}
- Reload VS Code:
Cmd + Shift + P
>Developer: Reload Window
- Open a Sass file e.g.
client/me/security-2fa-initial-setup/style.scss
- Mess up the formatting (e.g. use spaces for indentation.)
- Save the file and verify the problems are fixed.
Classes are the fundamental building block of our stylesheets. Using them appropriately and consistently is important to keep a maintainable and enjoyable codebase.
- Generic class names are deemphasized in favor of component-based structures. Instead of defining generic classes in a shared stylesheet, we construct components that come with both markup and styles ready to be used.
- Choose semantic class names based on hierarchy and content, not on positioning or visual appearance. (Example:
.site__meta-info
, instead of.site-menu-small
or.site-top-section
). - Avoid redundant bits of information that are provided by the HTML element or other contexts. (Don't do
.site__content-box
on adiv
, thebox
is not necessary.) - We keep one level of prefix for single components present in the class name:
.site__title
instead of.site .title
. This may seem verbose but it provides more flexibility, clear direct selectors, and immediate recognizability of its role in a large codebase like Calypso. Classes are not just for style purposes, they should provide meaning to the reader parsing the document. - Keep class names lean and to the point. Name classes the same way you would describe what the content of an element is to a stranger.
Calypso already provides helpers for many common solutions. Please, use them! We are transitioning towards a component-based structure where each React component will have its own stylesheet. However, there will be a few files that are by its nature shared resources across all components. (Colors, typography, some mixins, etc.)
- Don't use custom colors, always utilize what
_colors.scss
provides. If you have to set a color, use lowercase hex values, and shorten them to their smallest expression (like#aaa
). - Render icons using
<Gridicon>
. - Don't define your own media queries or breakpoints — use the provided
breakpoint( "value" )
mixin. - Calypso runs Sass with autoprefixer, that means you DON'T need to directly use vendor specific properties.
Currently, all component based Sass files are imported into the respective JavaScript sources (using import './style.scss'
statements). They are compiled by webpack as part of the bundling process into CSS chunks that are then loaded into the browser at runtime. Remember that all styles, even when loaded at different times, eventually end up on one page as part of a single HTML document. Make sure you namespace your styles for the page you are working on.
Under the hood, we are using webpack and its sass-loader
, for compiling the styles with node-sass
(a C++ implementation of the Sass compiler which is working on parity with the reference Ruby implementation) and mini-css-extract-plugin
, for creating the CSS chunks to be loaded as <style>
tags into the browser.
To avoid code bloat and have a more consistent experience we use the same breakpoints in all SCSS files whenever possible. DO NOT define your own media queries. We should use the break-*
mixins from Gutenberg. For example:
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/mixins";
.class-name {
margin-bottom: 8px;
padding: 12px;
@include break-mobile {
margin-bottom: 16px;
padding: 24px;
}
}
Furthermore, we are pushing for a mobile-first approach to media queries, meaning your default styles should apply to mobile, and desktop should build on top of that. You should avoid the use of max width breakpoints.
The old breakpoint-deprecated
mixin should be replaced with Gutenberg's break-*
mixins.
If you are adding a new Sass file (global or component-specific), you need to import it in some JavaScript source file. Try to avoid creating global styles and favor of styling particular components. JS and CSS code is async-loaded just in time when a particular section or an async-loaded React component is loaded.
- DO declare all of your
@import
dependencies at the top of a file that needs it/them. - DON'T
@import
dependencies in a global file and just hope it filters down to your partial.
- DON'T nest selectors in general. Exceptions are
:hover
/:focus
/::before
/.is-modifier
, and alike. - DO attempt to keep nesting to 2 levels deep at most.
- DO list items inside a selector in the following order (not all will necessarily be present):
@extend
(s).- property list for the element.
- mixin(s).
- nested selectors, with a space above each to keep them visually distinct.
Example of the above:
.parent {
@extend %awesomeness;
border: 1px solid;
font-size: 2em;
@include moar-awesome( true );
&::before {
// and so on
}
}
- DO use
@extend
when in doubt, using the%placeholder
syntax. This will produce leaner output. - DON'T use mixins for anything that doesn't accept an argument. This is
@extend
territory. - DO read this article if you don't understand
@extend
.
- DO make generous use of comments to explain the whys of what you are doing.
- DO use
// Comments
rather than/* comments */
. Multiline comments should be written like this:
// This is a comment
// in two lines
Add as much comments as needed to your Sass file, especially around clever code.
- DO use tabs for indents.
We're using RTLCSS to convert public/style.css
to rtl. This happens automatically during yarn run build-css
.
- If your css code refers to a filename with ‘left’ or ‘right’ in it, for example background:
url(arrow-left.png)
: the RTL version will reference a different file, e.g.background: url(arrow-right.png)
. Please make sure that file exists. - Same goes for ‘ltr’ and ‘rtl’ in it, for example background: url(icons-ltr.png): the RTL version will link to the other direction, e.g
background: url(icons-ltr.png)
. Please make sure that file exists. - If a part of the css needs to remain the same in the ltr version, mark it with a comment like this:
/*rtl:ignore*/
div.alignright {
float: right;
clear: right;
margin: 0.5em 0 0.8em 1.4em;
}
/*rtl:ignore*/
input.email {
direction: ltr;
}
If you need custom RTL css code, add it to your stylesheet with a .rtl class prefix, and the rtl ignore comment mentioned above. For example:
/*rtl:ignore*/
.rtl div.onlyinrtl {
font-family: Tahoma;
}
Note for either of the above that because of the SCSS build process, if you're nesting things then you might need to place the rtlignore comment inside the value, with Sass interpolation syntax
.rtl {
.onlyinrtl {
margin-right: 5px #{"/*rtl:ignore*/"};
}
}
You can also define specific values for RTL like so:
.class {
margin-right: 5px #{"/*rtl:2px*/"};
}
You can find more details in the RTLCSS documentation.
When defining positioning properties, indent the top/right/bottom/left one level below the position declaration for improved readability:
selector {
position: absolute;
left: 0;
top: 20px;
}
- DO NOT use spaces around parenthesis
@include breakpoint-deprecated(">480px") {
color: rgb(0, 0, 0);
transform: translate(-50%, -50%) scale(1);
}
When adding z-indexes use our scss z-index function. This means you'll need to
add another entry to the $z-layers
variable in
assets/stylesheets/shared/functions/_z-index.scss
.
Because browsers support a hierarchy of stacking contexts rather than a single global one, we use a tree with two levels of keys. The first is the name of the parent stacking context, and the latter is the selector for the stacking context you've just created. We keep each level of the tree sorted by z-index so we can quickly compare z-index values within a given stacking context.
An element creates a new stacking context when any of the following are true:
- It's the root element (
html
) - Has a position other than
static
, with az-index
value - Has one of the following CSS properties: (
transform
,opacity
< 1,mix-blend-mode
,filter
) - Has
position: fixed
- Has
isolation: isolate
- Has
-webkit-overflow-scrolling: touch
So, to add a new z-index:
- You'll want to make sure the element actually creates a stacking context
- Look up its parent stacking context
- Put the new rule in the
$z-layers
tree, using the selector of the parent stacking context as the initial key and the element's full CSS selector as the final key.
You can use this handy Chrome extension to find the stacking contexts of both the element and its parent.
As a simple example, imagine that we have a page with the following markup:
<div class="masterbar"></div>
<div class="modal">
<div class="modal__icon"></div>
<div class="modal__content"></div>
<div class="modal__header"></div>
</div>
With CSS of:
div {
position: relative;
}
.masterbar {
z-index: 2;
}
.modal {
z-index: 1;
}
.modal__icon {
z-index: 100;
}
.modal__content {
z-index: 300;
}
.modal__header {
z-index: 200;
}
Each div
in this case creates a stacking context, with the render order
summarized as:
1.0 .modal
1.100 .modal__icon
1.200 .modal__header
1.300 .modal__content
2.0 .masterbar
In this case icon, header, and content can never be rendered above the masterbar because it is contained within the modal stacking context, which has a lower value than the masterbar.
With this in mind our $z-layers
might look like the following, with z-index
values sorted from lowest to highest within a stacking context:
$z-layers: (
"root": (
".modal": 1,
".masterbar": 2,
),
".modal": (
".modal__icon": 100,
".modal__header": 200,
".modal__content": 300,
),
);
.modal__icon {
z-index: z-index(".modal", ".modal__icon"); // returns 100
}
While we can support a map with more than 2 layers, there isn't much benefit to doing
this. Stacking of children are completely resolved within their parent context. So in
our example the stacking of .modal__icon
, .modal__header
, and .modal__content
are
completely resolved within .modal
. Once this is done the entire .modal
element is
passed for stacking in the root element, with it's sibling .masterbar
.
For further reading on stacking contexts see: