In ScandiPWA, you need to add class names to elements in React, and then use those class names in CSS to select the elements and style them. But what names should we use for classes to ensure consistency? The answer, for ScandiPWA, is to follow the Block-Element-Modifier (BEM) methodology.
<aside> ✅ Benefits of following our BEM guidelines:
The BEM methodology is a system for naming CSS classes. This allows for building complex and flexible components while ensuring that class names do not clash. Where can I find the official BEM documentation?
<aside> ➡️ What does the name clashing mean? Suppose 2 different, unrelated components used the same CSS class without "knowing" it, then any developer trying to style one of the components would also affect the other, making behavior unpredictable and hard to manage. For this reason, we want different components to have different class names.
</aside>
A BEM class has 4 possible formats:
<Block>
<Block>_<Modifier>
<Block>-<Element>
<Block>-<Element>_<Modifier>
Note the "actors" of these formats:
isHollow
modifier to enable the hollow button variation. Boolean modifiers should start with is
or has
, to make their meaning clear.Field
component can have different type modifiers: type_text
, type_number
, type_radio
, etc.<aside>
➡️ Element and Modifier are optional in class names. The only required thing for each class is a block. Block can have modifiers (Button_isHollow
) or define sub-elements, like Header-Title
. Then each element may have its own modifier.
</aside>
What do BEM class names look like and how to read them?
Choosing the block is easy because it is always the same as the name of the component. So if you're working on a component called ProductCard
, the block name must also be ProductCard
<aside> ➡️ Note: the "main" element in your component (such as the Footer container) doesn't necessarily need an element – but most of the elements will need a BEM element to differentiate between them.
</aside>
Choosing the Element is like picking a variable name. You need to use a short name that correctly identifies the element you are naming. What is the purpose of the element you are naming?
As an example, the Footer
component (block), consists of several elements:
Content
Column
ColumnTitle - a h3 title for a column
ColumnContent
CmsBlockWrapper
CopyrightContentWrapper
Copyright
etc.
<aside> ➡️ It's ok to nest elements inside other elements – BEM is just a guide for how to name them!
</aside>
By "default", you shouldn't be thinking about what modifier to add. Modifiers should only be added when it is necessary. Consider the following cases:
Does the same element have multiple states?
An element might need to be styled differently depending on its current state.
A common requirement is specifying that an element is currently active. For example, the product page has multiple tabs below the main details. All tabs are the same kind of element, so they have the ProductTab-Item
class. But the currently active tab needs additional styles – so instead of duplicating code by creating another element, we use an isActive
modifier. The active tab gets an additional ProductTab-Item_isActive
class, and it can be styled as needed.
Are there multiple variations of the same element?
In that case, you might want to use the same Block-Element pair for all of them (as they might have some common styles) and use different Modifiers to differentiate between different variations.
For example, there are multiple Header-Button
elements, and they have some styles in common. But the buttons have different purposes, so they are separated by adding different modifiers. The Header-Button_type_minicart
is a button with a mini cart icon and Header-Button Header-Button_type_share
is a "share" button.
<aside>
➡️ Even if you add a modifier, the original Block-Element class will still be added. That is, if you use ProductTab-Item_isActive
, then ProductTab-Item
is automatically used as well.
</aside>
In ScandiPWA JSX, specifying the block, element, and modifier is very easy – just pass them as props! For example, to specify Footer
as the block and Column
as the element, use:
<div block="Footer" elem="Column"> ...
Then, these props will be automatically converted into the corresponding BEM class, Footer-Column
Specifying the modifiers is also easy. Simply pass an object to the mods
prop. For example, to add the isActive
modifier, use:
mods={ { isActive: true } }
...and to add type_text
, use:
mods={ { type: 'text' } }
<aside>
➡️ Why the strange double-brace ({ { } }
) syntax? The first {
is just to "escape" from JSX notation and enter a JavaScript expression – you need to use it whenever you pass any value (except a string) to a prop. Then, the expression is a simple object, so { type: 'text' }
. It's just 2 completely different uses of the same symbols, in the same place.
</aside>
Look at the render
method of ProductCard
:
// component/ProductCard/ProductCard.component.js (excerpt)
render() {
const {
children,
mix,
isLoading
} = this.props;
return (
<li
block="ProductCard"
mix={ mix }
>
<Loader isLoading={ isLoading } />
{ this.renderCardWrapper((
<>
<figure block="ProductCard" elem="Figure">
{ this.renderPicture() }
</figure>
<div block="ProductCard" elem="Content">
{ this.renderReviews() }
{ this.renderProductPrice() }
{ this.renderVisualConfigurableOptions() }
{ this.renderTierPrice() }
{ this.renderMainDetails() }
{ this.renderAdditionalProductDetails() }
</div>
</>
)) }
<div block="ProductCard" elem="AdditionalContent">
{ children }
</div>
</li>
);
}
<aside> ➡️ Note that the block is always the same as the name of the component. This ensures consistency and prevents name clashes.
</aside>
To add modifiers, pass an object with modifiers to the mods
prop. Boolean modifiers will be automatically detected and treated as such.
renderMainDetails() {
const { product: { name } } = this.props;
return (
<p
block="ProductCard"
elem="Name"
mods={ { isLoaded: !!name } }
>
<TextPlaceholder content={ name } length="medium" />
</p>
);
}
Instead of directly selecting the.Footer-Column
class, it is better to "group" all the.Footer
selectors, and use &
concatenation (provided by SCSS) to select different elements. This makes your code easier to read, especially when there are multiple elements. Example:
// The Block selector
.Footer {
// Nested are the Element selectors
// & is automatically replaced by the parent selector.
// So this will magically become .Footer-CopyrightContentWrapper
&-CopyrightContentWrapper {
background-color: var(--secondary-base-color);
}
&-CopyrightContent {
padding: 10px 0;
display: flex;
&_isHidden {
display: none;
}
}
&-Copyright {
font-size: 12px;
text-align: center;
color: var(--secondary-dark-color);
padding-inline: 16px;
}
&-Content {
min-height: var(--footer-content-height);
background-color: var(--secondary-base-color);
}
&-Column {
@include mobile {
width: 100%;
}
}
}
Another example:
// component/ProductCard/ProductCard.style.scss (excerpt, annotated)
.ProductCard {
// style the block
padding-left: 0;
min-width: 0;
&::before {
content: none;
}
// & will get replaced with the parent selector, .ProductCard.
// so this selects .ProductCard-Content (the Content element
// of the ProductCart block
&-Content {
padding: 1rem;
display: flex;
flex-wrap: wrap;
padding-top: 23px;
}
&-Brand {
font-weight: 300;
opacity: .5;
}
&-Figure {
flex-grow: 1;
}
&-Name {
width: 100%;
font-size: .9rem;
// this selector will compile to .ProductCard-Name_isLoaded
&_isLoaded {
text-overflow: ellipsis;
}
}
}
ScandiPWA defines certain breakpoints that enable you to write viewport width-specific styles.
Selector | Visible on Mobile | Visible on Tablet | Visible on Desktop |
---|---|---|---|
desktop |
✔️ | ||
before-desktop |
✔️ | ✔️ | |
tablet |
✔️ | ||
tablet-landscape |
landscape only | ||
after-mobile |
✔️ | ||
mobile |
✔️ |
To use a breakpoint, use include
:
// ...
&-Brand {
font-weight: 300;
opacity: .5;
// will only affect mobile devices
@include mobile {
line-height: 1;
font-size: 12px;
}
}
CSS variables are useful when:
CSS variables are always defined in :root
. That way, re-defining them anywhere else is an easy way to override them. Example:
//component/CartItem/CartItem.style.scss (simplified & annotated)
// we define variables in :root
:root {
--cart-item-background: #fff;
--cart-item-actions-color: #000;
}
.CartItem {
&:hover {
// we can re-define them to override values
--cart-item-actions-color: #222
}
&-Wrapper {
background: var(--cart-item-background);
}
&-Delete {
height: 35px;
color: var(--cart-item-actions-color);
}
}
<aside> ➡️ ScandiPWA uses an auto-prefixer. When compiling, vendor-specific versions of rules are added to make sure they work on most browsers.
</aside>
Sometimes, you may want to allow other components to add additional style rules to a component. For example, the Image component needs to define some styles, but can't predict ahead of time the exact styling features that will be needed for Images in parent components.
The solution is to allow other components to add their own styles to the Image component. The BEM methodology allows this by "mixing" 2 BEM classes together. For example, in the CategoryDetails component, in addition to the regular Image
block, the CategoryDetails-Picture
class will be added. Since the element will now have both of these classes, the parent component can additionally style the element with new rules.
Example: mixing the BEM class of the Image component with a custom BEM class:
renderCategoryImagePlaceholder() {
return (
<Image
mix={ { block: 'CategoryDetails', elem: 'Picture' } }
objectFit="cover"
ratio="custom"
isPlaceholder
/>
);
}
<aside>
➡️ The mix
functionality is not automatic! If you want to create a component whose styles can be mixed with a custom class, you need to accept the mix
prop and pass it on to the container element.
</aside>