Imarc

Block Property Modifier: A BEM-like CSS custom properties methodology

Kevin Hamer, Principal Web Engineer
Posted on Apr 13, 2022

CSS custom properties are here. Now that we know how they work, let’s look at a methodology for keeping them organized.

CSS custom properties (or CSS variables) are powerful, cascade, and let us build websites in ways we couldn’t before. That said, the cascading of CSS means there’s a good chance of unexpected side effects, especially if you’re defining properties at more than just the :root level.

BPM (Block Property Modifier) is a recommendation for using custom properties with BEM (Block Element Modifier ), ABEM (Atomic Block Element Modifier), or almost any block-based CSS methodology. Its conventions are focused on being clear about the intended scope for custom properties so that they can be used easily between components or refactored without unexpected consequences.

1. --<somePrefix>-<property-name>-<someSuffix>

When choosing property names, consider using the name of an existing CSS property. For example, color over text-color, gap over spacing, etc. If no CSS property is appropriate, choose a name that feels consistent with other CSS properties.

Developers are used to CSS properties, so they are more likely to recognize what a property is if you're referencing a property they already know.

Use suffixes if you have multiple similar values: color-primary, color-secondary, gradient-start, gradient-end, font-family-heading, etc.

2. Prefix :root properties.

Pick a standard prefix for properties defined within :root – like --root-:

:root {
  --root-color-primary: orangered;
  --root-color-secondary: cyan;
  --root-padding: 20px;
  --root-buttonAngle: -15deg;
}

You should override these values only at the :root element level – not within specific elements. These values act like constants for your project and should be accessible from any component. They may not actually be constant – you may want to make these values change based on media queries like prefers-color-scheme, prefers-reduced-motion, or viewport size – but we want these values to be available everywhere without it being affected by any parent element.

3. Prefix private properties with their blockName.

Prefix private, block properties with their blockName:

.button {
  --button-color: purple;
  --button-buttonAngle: var(--root-buttonAngle, 15deg);
}

There’s no way to make true private CSS properties. However, if you’re working within a methodology like Atomic Design, components should rarely appear as parents of themselves, so prefixing properties with their block names will avoid collisions. These properties should not be set or used outside of this block. Developers should be free to create and refactor these. They should be set or used by the block, block’s elements, and modifiers:

.button {
  --button-color: purple;
  background-color: var(--button-color);
  box-shadow: 0 0 10px var(--button-color);
}
// BEM
.button--bright {
  --button-color: gold;
}
// ABEM
.button.-bright {
  --button-color: lemonchiffon;
}

4. Use unprefixed properties for public properties.

First, let’s say you have a .card block and you want to set a --color for child blocks .button and .quote:

.card {
  --color: slateblue;
  --card-color: var(--color);
  border: 4px solid var(--card-color-primary);
  box-shadow: 0 0 10px var(--card-color-primary);
}
.button {
  --button-color: var(--color, var(--root-color-primary));
  background-color: rgba(255, 255, 255, .5);
  border: 3px double var(--button-color);
  color: var(--button-color);
}
.quote::before {
  color: var(--color, var(--root-color));
  content: '"';
}

Here, .button and .quote both use --color as defined by .card. We leave these properties unprefixed to make it clear that public, unprefixed properties can be set or used by any block.

Keep your public and private properties separate. If we try to refactor to get rid of --button-color for example:

.button {
  background-color: rgba(255, 255, 255, .5);
  border: 3px double var(--color, var(--root-color-primary));
  color: var(--color, var(--root-color-primary));
}
// We can't do this anymore
.button.-danger {
  --button-color: brickred;
}
// Instead we have to override every place the color is used
.button.-danger {
  border-color: brickred;
  color: brickred;
}
// We can override over the original --color, but now we can't get
// the original value of 'slateblue' at all.
.button.-danger {
  --color: brickred;
}

5. Be consistent

Reuse property names. For example, having all three of --root-color, --button-color, and --color defined is great. Reusing the same public properties for different components means all those components will work better together.

Examples

Setting a property for a specific child block

Let’s say within the .card we only want to change the --color of one of the buttons. We can set the public property for a specific element:

.card__action {
  --color: yellowgreen;
}

Reverting a custom property

Similarly, we can revert a property to its default value using initial:

// Applies to all elements
.card {
  --color: yellowgreen;
}
// But initial reverts card__action to its defaults
.card__action {
  --color: initial;
}

Changing values for public properties for child elements

Say both .card and .button use --color. If they should have the same value, nothing special needs to be done; the value of --color will just cascade down. However, if you want a different value for --color for child elements without changing the value for .card, we can do this:

.card * {
  --color: lime;
}
// or if you want lower specificity:
:where(.card *) {
  --color: lime;
}

We can also unset the value of --color entirely for child elements, by using initial:

// If specificity is an issue, target :where(.card *)
.card * {
  --color: initial;
}
// or if you want lower specificity:
:where(.card *) {
  --color: initial;
}

Priority of Custom Properties vs. Modifiers

Going back to the .button example, we have two different ways we can make modifiers interact with custom properties:

.button {
  --button-color: var(--color, var(--root-color-primary));
  background-color: rgba(255, 255, 255, .5);
  border: 3px double var(--button-color);
  color: var(--button-color);
}
.button.-danger {
  --button-color: brickred;
}
.button.-bright {
  --button-color: var(--color, gold);
  border-width: 6px;
}

.button.-danger changes the button color and will ignore the value of --color entirely. If you’re implementing light and dark modes, you may want to override a custom property so that the same color (brickred) is used regardless.

.button.-bright on the other hand, still respects the value of --color while changing the default color. We could use -bright even if we need the color to change, or want the new color (gold) for light mode but need to override gold for dark mode.

Private Properties for Block Elements

If you have a need for it, you can prefix variables with a blockName__elementName instead of just a blockName:

.card__hero {
  --card__hero-width: calc(50vw + 600px);  
  width: var(--card__hero-width);
}

When you define properties like this, you cannot override them from .card unless you define accompanying public properties:

.card.-thin {
  --hero-width: 30vw;
}
.card__hero {
  --card__hero-width: var(--hero-width, calc(50vw + 600px));
  width: var(--card__hero-width);
}

If you need to override a property from the block, it’s recommended to make these private properties on the block instead:

.card {
  --card-width-hero: calc(50vw + 600px);
}
.card.-thin {
  --card-width-hero: 30vw;
}
.card__hero {
  width: var(--card-width-hero);
}

Starting off with this method

Consider adopting this convention as you first adopt custom properties . Try to skip the “Wild West” phase of developing with new features, and consider how to organize custom properties with the rest of your CSS.

Have questions about this topic, or want to learn more? Let’s talk.