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.