Styling Components

ScandiPWA uses the BEM methodology and SCSS

To style components, ScandiPWA uses the Block-Element-Modifier (BEM) methodology. BEM is based on assigning meaningful and unique class names to each HTML element we want to style while ensuring that we never need to use any CSS nesting in selectors.

We strive to follow this methodology in the core ScandiPWA theme, and we strongly encourage you to use it when overriding this theme as well.

Benefits of folowing our BEM guidelines:

  • The codebase is consistent

  • Styles are maintainable and can be re-used by composition with mixes

  • Styles can be overriden easily in child themes

The BEM Methodology

This section should serve as a quick introduction to how BEM is used in ScandiPWA. You can read the official BEM Guide. ScandiPWA uses the React variation.

All primary BEM classes are composed of these 2 parts:

  • <Block> - in UpperCamelCase, the name of the component this element belongs to

  • -<Element> (can be left out) - in UpperCamelCase, a meaningful name of this element (e.g. Container, Button, Divider, Image... whatever identifies the purpose of this element)

Blocks indicate which component owns this element and preserves uniqueness accross all components. By keeping it the same as the component name, you don't need to worry about name-clashes with other components.

The "main" HTML element of each block may be left without a BEM Element. All other elements should have one to indicate their function.

In addition to its primary <Block[-Element]> class, a block or element may have any number of modifiers of the form:

  • <Block[-Element]>_<booleanModifier> - for boolean modifiers such as isActive, isVisible, isBold, etc. The presence of this modifier indicates that it is "true", and it's absence indicates that it is "false".

  • <Block[-Element]>_<modifierName>_<someValue> - for boolean modifiers with values such as color_red, type_primary, size_thumbnail

BEM modifiers can be used to indicate state, available actions, or to distinguish between similar instances of the same element.

BEM in JavaScript

Formatting BEM classes manually with className would get repetitive, so ScandiPWA uses rebem-jsx to be able to use block and elem to specify the class. As an example, 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>
        );
    }

Note that the block is always the same as the name of the component. This ensures consistency and prevents name clashes.

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

Styling Components in SCSS

Selecting BEM

We can take advantage of SCSS amperstand operator to reduce the repetitiveness of selecting BEM classes:

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. These can be found in the global style directory. To select a specific device, simply use the @include directive:

// ...
    &-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:

  • You want to reuse the same value multiple times

  • You want to be able to override a value based on the context

  • You want to make it more clear what a value represents by naming it

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

ScandiPWA uses an auto-prefixer. When compiling, vendor-specific versions of rules are added to make sure they work on most browsers.

Mixes

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.

component/CategoryDetails/CategoryDetails.component.js
    renderCategoryImagePlaceholder() {
        return (
            <Image
              mix={ { block: 'CategoryDetails', elem: 'Picture' } }
              objectFit="cover"
              ratio="custom"
              isPlaceholder
            />
        );
    }

Note: to allow a component's styles to be mixed, you need to pass the mix prop to an element in the Component – this won't happen automatically. For example, consider how the Image component passes on the mix prop:

component/Image/Image.component.js (simplified excerpt)
    render() {
        const {
            mix
        } = this.props;


        return (
            <div
              block="Image"
              mix={ mix }
            >
                { this.renderImage() }
            </div>
        );
    }

Last updated