Save the Children code was written with the following coding standards in mind.
These standards are based on the BEM code writing and organization model. Please see the video presentation (opens new window) to learn what BEM is and why we use it. Whenever possible, use design components (opens new window).
An illustrated guide (opens new window) should help you get started.
The HTML rendered by Luminate platform does not follow these standards and we cannot change it. But we still follow the BEM principles in organization of the code whenever possible. When it's not possible, then the definitions are placed into files that describe either a visual component (opens new window) or a page type (opens new window). There may have been some mistakes with BEM that were made early on, whenever possible we correct those to the standard as revisions are introduced.
The document consists of the followng sections:
# Spacing
- Intent code using 2 spaces for each level of indentation
- Use one selector per line when a ruleset has a group of selectors separated by commas.
- The opening brace (
{) of a ruleset's declaration block should be on the same line as the selector (or the same line as the last selector in a group of selectors.) The opening brace should include a single space before it. - Place the closing brace (
}) of a ruleset in the same column as the first character in the selector of the ruleset.
button,
.button {
}
- Include one declaration per line in a declaration block.
button,
.button {
background-color: palette('button', 'background');
color: palette('button', 'text');
}
- Each declaration should be indented one level relative to its selector.
- Each declaration block should be separated by one blank line. This applies to the nested ones as well. The spacing enables faster and easier reading and comprehension of the code.
figure {
margin: 0 0 px-to-rem($base-line-height / 2);
max-width: 100%;
&.figure--right {
margin: 0 0 px-to-rem($base-line-height / 2) px-to-rem($base-line-height / 2);
}
}
figcaption {
font-family: $caption-font-family;
}
- There MUST NOT be any whitespace (spaces or tabs) at the end of lines.
# Comments
- Provide comments for easy understanding, especially for the complex definitions. Because comments are removed during the complilation there is no limit to how many you can write. Be generous with your comments, write them with other developers in mind, using complete sentences to explain what's going on.
# Organization
- General pattern for the code organization. This pattern, once understood, lends iteslf to easy code reading, debugging, and extension.
.element-being-styled {
// 1. Default behavior rules for the element
// 2. Element's behavior at different resolutions
@include breakpoint(tablet-narrow) {
// Ideal order from mobile to desktop, but may differ based on definitions
// use either ascending or descending order for ease of reading and comprehension
}
// 3. Pseudo elements' definitions, if any
// Multiple pseudo elements should be listed in alpha order
&::after {
// 3a. Pseudo-elements' behavior at different resolutions
@include breakpoint(tablet-narrow) {
}
// 3b. Modifer classes as they apply to the pseudo element, avoid if possible
&.element-being-styled--active {
}
// 3c. Context for the elements, avoid this if possible
.parent-element & {
}
}
// 4. Element variants such as first or last child or state such as hover
&:hover,
&.js-active,
&:first-child,
&:last-child {
// 4a. Element state change at various resolutions
@include breakpoint(tablet-narrow) {
}
// 4b. The state change when the element-being-styled in context
// In rare cases, when an element with specific properties
// also needs to be restricted by context. Use with caution.
// Multiple parents should be listed in alpha order
.parent-class & {
// 4bi. Behavior of the tightly constrained element different resolutions
@include breakpoint(tablet-narrow) {
}
}
}
// 5. Element state with a custom class assigned
// This should be avoided if possible, however when coding in Luminate this is often needed
// For BEM code the modifier classes should be defined in a separate CSS block
&.element-being-styled--active & {
// 5a. Element state change at various resolutions
@include breakpoint(tablet-narrow) {
}
// 5b. Element custom behavior in context
// This should be avoided, if possible, as it brings in a lot of complexity
.parent-class & {
// 5bi. Element custom behavior in context at various resolutions
// This should be avoided, if possible
@include breakpoint(tablet-narrow) {
}
}
}
// 6. Element's styling when it appears in specific context
// Multiple parents should be listed in alpha order, so they are easy to locate
// For BEM code, the context should not be defined, rather a modifier class
// on an element-being-styled should be defined
// 6a. Grandparents; if there are multiple levels parents
// they should be all listed inline instead of nested for ease of reading.
// Regardless of the levels the blocks should be ordered by alpha
.gradnparent-class .parent-class & {
// 6ai. Behavior of the element-being-styled in context as specific resolutions
@include breakpoint(tablet-narrow) {
}
}
// 6b. Parent context can be set as follows
// Regardless of the levels the blocks should be ordered by alpha
.parent-class & {
// 6bi. Behavior of the element-being-styled in context as specific resolutions
@include breakpoint(tablet-narrow) {
}
}
// 6c. Preceding siblings are at times needed, they should be placed together with the
// parent and grandparent blocks in alpha order as the syntax is very similar
.sibling-element + & {
// 6ci. Behavior of the element-being-styled in context as specific resolutions
@include breakpoint(tablet-narrow) {
}
}
}
- Alphabetize all rulesets.
button,
.button {
// rules that start with a
background-color: palette('button', 'background');
border: 1px solid palette('button', 'border');
color: palette('button', 'text');
text-transform: uppercase;
z-index: 10; // rules that start with z
}
- Alphabetize all declaration blocks.
Note: for ease of finding elements in SASS, we ignore the first non-letter character in alphabetization.
// we ignore #, since all elements have child__ b is the first in order
#child__birthday {
order: 1;
}
.child__container {
display: flex;
}
.child__interests {
background-color: palette('child-interest' 'bg');
}
// photo is the last in the alphabetized list
.child__photo {
border: 1px solid palette('child-photo', 'border');
display: block;
}
- Whenever possible use classes to define styles. Based on the BEM principles the element with the same class should appear the same, regardless of the position. However, with an HTML rendered by the application, namely Luminate Online, that's not always possible. Use ids and set context only if the standalone class option is not viable.
- The element that is being styled should always appear at the start of the line, the content should always be set inside only if absolutely necessary. This enables an easy tracking down of the styles for the element. This also simplifies code maintenance as addition and exception to the element styling will be easy to write without rewriting the code.
If we have the following HTML (based on an actual example)
<body class="donation donation--1620">
<div class="donation-levels">
<div class="donation-level">
<label for="donation-level-35">$35.00</label>
<input id="donation-level-35" type="checkbox" value="35" />
</div>
<div class="donation-level">
<label for="donation-level-45">$45.00</label>
<input id="donation-level-45" type="checkbox" value="45" />
</div>
<div class="donation-level donation-level--other">
<label for="donation-level-other">Other</label>
<input id="donation-level-other" type="text" value="" />
</div>
</div>
</body>
Styling of the donation levels will be as follows
// We are styling a default donation level label
label {
.donation-level & {
background-color: palette('donation-level', 'bg');
color: palette('donation-level', 'text');
padding: px-to-rem(10px) px-to-rem(20px);
}
// Here the label looks different on a form 1620
.donation--1620 .donation-level & {
background-color: palette('donation-level', 'bg-alt');
color: palette('donation-level', 'text-alt');
padding: px-to-rem(10px) px-to-rem(20px);
}
// More context-based variants can go here
}
// We are styling an input
input[type="checkbox"] {
.donation-level & {
display: none;
}
}
To achieve the same styling the common way people write SCSS the code will look like this & will have to be studied first. Overwriting will also be difficult once more thatn 2-3 different rules will have to apply to the label or input.
DO NOT write this
// It's unclear what is being styled here without studying
.donation--1620 {
.donation-level {
label {
background-color: palette('donation-level', 'bg-alt');
color: palette('donation-level', 'text-alt');
padding: px-to-rem(10px) px-to-rem(20px);
}
}
}
// Not easy to see at a glace that it's the label we are styling
.donation-level {
label {
background-color: palette('donation-level', 'bg');
color: palette('donation-level', 'text');
padding: px-to-rem(10px) px-to-rem(20px);
}
}
- Minimize nesting in SCSS, there should be as few nestings as possible and never more than 3 levels deep.
- In cases where multiple levels of nesting required the following rules apply. In any styling tree there should be only one instance of
&selector&selector &and if necessary one@include breakpointonly instance. Selectors should be combined into one line for ease of reading, rather than being stacked in a tree. Any SCSS definition tree should not be more than 3 levels deep. Pattern:
element{
&.element-classes {
}
&::pseudo-elements {
.parent-classes & {
@include breakpoint(desktop-narrow)) {
color: black;
}
}
}
}
DO
label {
// Can easily tell at a glance that `::before` appears before the label
// and that we are styling that element here
&::before {
// there may be default definition here if needed
// then we define how the element is restyled in various contexts
.donation-level-user-entered & {
content: 'Enter Your Amount';
}
.donation-form-1640 .donation-level-user-entered & {
content: 'Your Amount'
}
.payment-type-option & {
content: '';
}
}
}
// Multiple selectors of the same level are combined, can be read and understood at a glance
label {
&.donation-level::before {
content: '$';
}
}
label {
// Label in the donation-level container (generic case)
.donation-level & {
background-color: palette('donation-level', 'bg');
color: palette('donation-level', 'text');
padding: px-to-rem(10px) px-to-rem(20px);
}
// Here the label looks different on a form 1620,
// a more complex case but can be read and understood fairly fast
.donation--1620 .donation-level & {
background-color: palette('donation-level', 'bg-alt');
color: palette('donation-level', 'text-alt');
padding: px-to-rem(10px) px-to-rem(20px);
}
}
DON'T DO
// Does ::before appear before the label or before
// .donation-level-user-entered? Hard to tell at a glance.
label {
.donation-level-user-entered & {
&::before {
content: 'Enter Your amount';
}
}
.payment-type-option & {
&::before {
content: '';
}
}
}
// This requires a study and based on a compiler may be compiled in a way
// that may result in invalid CSS
label {
&.donation-level {
&::before {
content: '$';
}
}
}
label {
// Label in the donation-level container (generic case)
.donation-level & {
background-color: palette('donation-level', 'bg');
color: palette('donation-level', 'text');
padding: px-to-rem(10px) px-to-rem(20px);
// This big nesting is unnecessary as now the code has to be studied
// to identify which parent will be printed first.
// Understanding the inheritance will be difficult as well.
.donation--1620 & {
background-color: palette('donation-level', 'bg-alt');
color: palette('donation-level', 'text-alt');
padding: px-to-rem(10px) px-to-rem(20px);
}
}
}
# Media Rules
- Media rules should be written within each element's style definitions and not as one block or file. This patterns allows us to focus and style or debug any element at all resolutions in one place.
.logo {
line-height: px-to-rem($h1-font-size);
margin: 0;
width: 70%;
@include breakpoint(tablet-narrow) {
width: 50%;
}
@include breakpoint(desktop-narrow) {
width: 24%;
}
}
- All styles should be written with a mobile first approach.
- If the code is simpler when desktop first approach is used, write the rules using the desktop first approach.
// Here most the link looks the same on all resolutions,
// except a few styles on tablet & mobile, so the code is written
// in a desktop first approach
.menu__link {
font-size: px-to-rem(15px);
font-weight: bold;
line-height: px-to-rem(15px);
text-decoration: none;
text-transform: uppercase;
@include breakpoint('under-desktop') {
display: flex;
padding: px-to-rem($base-line-height / 2) 2%;
}
}
- In cases where the mobile and desktop display are drastically different feel free to put all styles into media breakpoints and not override multiple styles. Just ensure that you have full coverage for all breakpoints (0px to infinitely large screen).
// In this case the mobile menu has a lot of styles we don't need on desktop,
// so we only assign those styles to the appropriate breakpoints only.
.menu--main {
@include breakpoint('under-desktop') {
background: palette('menu', 'bg--mobile');
box-shadow: 0 5px 5px palette('menu', 'box-shadow--mobile');
display: none;
flex-direction: column;
width: 100%;
}
@include breakpoint('desktop-narrow') {
justify-content: space-between;
}
}
- Media rule should be the last innermost definition and should not contain any additional selectors inside.
DON'T DO
.form__column--80-100 {
@include breakpoint('under-tablet-wide') {
margin-bottom: px-to-rem($base-line-height / 4);
width: 100%;
&:last-child {
margin-bottom: 0;
}
}
}
DO
.form__column--80-100 {
@include breakpoint('under-tablet-wide') {
margin-bottom: px-to-rem($base-line-height / 4);
width: 100%;
}
&:last-child {
@include breakpoint('under-tablet-wide') {
margin-bottom: 0;
}
}
}
When the code is written according to the pattern it's easy to extend, read and reason about. Here's how this code can and should be extended with new style definitions.
.form__column--80-100 {
// Default element behavior
@include breakpoint('under-tablet-wide') {
margin-bottom: px-to-rem($base-line-height / 4);
width: 100%;
}
// Element behavior when it's a last child
// All logic for the last child goes into one block
&:last-child {
@include breakpoint('under-tablet-wide') {
margin-bottom: 0;
}
@include breakpoint(tablet-only) {
border-bottom: 1px solid $grey;
}
.form--start-project & {
@include breakpoint('under-tablet-wide') {
margin-bottom: 10px;
}
}
.form--questionnaire & {
border-bottom: 1px solid $grey;
}
}
}
# Variables
- All colors, font families, base font sizes, base line height sizes should be set in the _variables.scss (opens new window) file.
- All written code should rely on variables as much as possible.
- All background, text, and border colors should be set using the name-based map.
// in _variables.scss
$palettes: (
a: (
'text': $teal-dark,
'text--hover': $navy
),
)
// in _a.scss
a {
color: palette('a', 'text');
&:hover {
color: palette('a', 'text--hover');
}
}
- All breakpoints should be set using the predefinied breakpoints, which will retrieved from the map using a mixin.
// in _variables.scss
$breakpoints: (
'desktop-middle': '(min-width: 1024px)',
'desktop-narrow': '(min-width: 960px)',
'desktop-wide': '(min-width: 1200px)',
'desktop-widest': '(min-width: 1600px)',
'desktop-narrow-to-wide': '(min-width: 960px) and (max-width: 1199px)',
'phone-landscape': '(min-width: 480px)',
'phone-landscape-to-tablet-wide': '(min-width: 480px) and (max-width: 799px)',
'print': 'print',
'tablet-middle': '(min-width: 768px)',
'tablet-narrow': '(min-width: 600px)',
'tablet-only': '(min-width: 600px) and (max-width: 959px)',
'tablet-small-only': '(min-width: 600px) and (max-width: 799px)',
'tablet-wide': '(min-width: 800px)',
'tablet-middle-to-desktop': '(min-width: 768px) and (max-width: 959px)',
'tablet-wide-to-desktop': '(min-width: 800px) and (max-width: 959px)',
'tablet-wide-to-desktop-wide': '(min-width: 800px) and (max-width: 1199px)',
'under-desktop': '(max-width: 959px)',
'under-desktop-wide': '(max-width: 1199px)',
'under-desktop-widest': '(max-width: 1599px)',
'under-phone-landscape': '(max-width: 479px)',
'under-tablet': '(max-width: 599px)',
'under-tablet-middle': '(max-width: 767px)',
'under-tablet-wide': '(max-width: 799px)',
'ie11': '(-ms-high-contrast: none), (-ms-high-contrast: active)',
);
// in _menu.scss
.menu--main {
@include breakpoint(under-desktop) {
background-color: palette('menu--main', 'bg');
display: flex;
flex-direction: column;
//height: 0;
overflow-x: hidden;
overflow-y: visible;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s 0s;
width: 0;
}
@include breakpoint(desktop-narrow) {
justify-content: space-between;
width: 100%;
}
}
# Standard mixins
- adjust-font (opens new window) & dependency units (opens new window) mixins are deprecated use,
px-to-reminstead - breakpoint mixin (opens new window) for retrieving a breakpoint from a mapping.
- palette mixin (opens new window) for retrieving a color from a mapping for a defined element.
- px-to-rem mixing (opens new window) for converting pixels to rem units.
- svg-icon mixin (opens new window) for rendering SVG icons with CSS whenever image insertion is not available
- visually hidden mixin (opens new window) for hiding accessible text from the visual users
# Style reset
- When we write styles from scratch we normally use a
_normalize.scss(opens new window) file to do some browser style resets that prevent the elements from appearing the same through the sites.
# Browser hacks
- Use only when necessary, please verify that you have implemented valid CSS first before adding any overrides.
- The use of
!importantis not allowed in the code, the CSS inheritance rules should be used to override styles. The only valid case for using!importantit to override an inline style produced by a third party system or another CSS definition with!importantin the third-party system that cannot be removed.