What is BEM?

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>

What does a BEM class consist of?

A BEM class has 4 possible formats:

Note the "actors" of these formats:

<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?

Examples

How to choose a BEM class?

1. Choose the block

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

2. Name the element

<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:

<aside> ➡️ It's ok to nest elements inside other elements – BEM is just a guide for how to name them!

</aside>

3. Add a modifier if necessary

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:

<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>

How to specify the BEM in ScandiPWA?

How to set the class in JSX?

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>
    );
}

How to use the class in CSS?

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;
        }
    }
}

Breakpoints

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

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>

How to mix several BEM classes?

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>