JavaScript is required for this page to work.
⤎ Back to blog overview

Using BEM (CSS) in Vue and Nuxt applications

I think BEM aligns naturally with Vue’s single-file-component structure, since each component encapsulates its logic and styles. The methodology has been around for many years now and has proven to be an effective way of organizing and writing CSS. Having to come up with appropriate class names also helps me to think about the semantics and structure of my HTML and makes the code more readable. In this post I will go briefly through what BEM is and how it can be used in your Vue components.

What is BEM?

BEM (Block, Element, Modifier) is a methodology that helps you systematically name your CSS classes (and as a side-effect, think about your HTML's organization, I find). It is a style of writing CSS so to speak. There is no npm package and no config (more on that later).

When you write BEM style CSS you have to get into the habit of thinking about components as blocks, smaller parts within those components as elements and the classes that change the default state or look of your component as modifiers. Below I will use a card component as an example – but I might as well be using examples of other typical components like header, footer, button, hero-banner, navigation etc. What blocks you have in your project will essentially depend on what components you have in your project.

Vue
<template>
  <div class="card card--primary">
    <h2 class="card__title">Title</h2>
    <p class="card__content">Content</p>
    <button class="card__button">Click me</button>
  </div>
</template>

<style>
.card {
  background-color: white;
  border-radius: 8px;
  border: 1px solid black;
  padding: 8px 16px;
}

.card__title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 12px;
}

.card__content {
  font-size: 16px;
}

.card__button {
  border: 1px solid black;
  padding: 2px 8px;
  background-color: white;
  font-size: 12px;
}

.card--primary {
  border-color: blueviolet;
}

.card--primary .card__button {
  border-color: red;
}

.card--primary .card__button:hover {
  background-color: blueviolet;
}
</style>

In the example above, .card is the block, .card__title, .card__content and .card__button are the elements and .card--primary is the modifier.

In a more real-life example, the button would likely be its own component that we would use inside of the card component.

As you can now see, the primary rules are rather simple. The main block element gets a descriptive class name (in this case .card), the elements within the block will get class names that combine the main block name with a descriptive element name, joined together by two underscores (for example .card__title) and each modifier is a combination of the block it is modifying with a descriptive modifier name, joined by two dashes (for example .card--primary).

Using Tailwind and BEM together

I am not going to discuss this much since I do not use Tailwind (or other utility CSS frameworks) very much myself, but you can combine BEM style CSS with Tailwind by using @apply. This would inject Tailwind's utility classes into your class selectors. It could look something like this:

Vue
<template>
  <div class="card card--primary">
    <h2 class="card__title">Title</h2>
    <p class="card__content">Content</p>
    <button class="card__button">Click me</button>
  </div>
</template>

<style>
.card {
  @apply bg-white rounded-lg border border-black p-2 sm:p-4;
}

.card__title {
  @apply text-xl font-bold mb-3;
}

.card__content {
  @apply text-base;
}

.card__button {
  @apply border px-2 py-1 bg-white text-sm;
}

.card--primary {
  @apply border-blueviolet;
}

.card--primary .card__button {
  @apply border-red;
}

.card--primary .card__button:hover {
  @apply bg-blueviolet;
}
</style>

The benefits of writing BEM with Less (or other preprocessors like Sass

Now I will get back to my point from above about no npm package and no config being needed. The thing is that BEM really shines when combined with a preprocessor like Less or Sass.

Nesting capabilities

First, BEM’s structured naming conventions work exceptionally well with the nesting capabilities of preprocessors. For instance, Less allows you to nest elements and modifiers within their parent block, keeping related styles grouped logically. This makes the code more readable I find and easier to maintain by showing the clear relationship between blocks and elements within it. When the preprocessor has done its job, the same low specificity CSS comes out on the other end by concatenating the parent class with the indented post-fixes. You can read about how that works here.

Vue
<template>
  <div class="card card--primary">
    <h2 class="card__title">Title</h2>
    <p class="card__content">Content</p>
    <button class="card__button">Click me</button>
  </div>
</template>

<style lang="less">
.card {
  background-color: white;
  border-radius: 8px;
  border: 1px solid black;
  padding: 8px 16px;

  &__title {
    font-size: 24px;
    font-weight: 700;
    margin-bottom: 12px;
  }

  &__content {
    font-size: 16px;
  }

  &__button {
    border: 1px solid black;
    padding: 2px 8px;
    background-color: white;
    font-size: 12px;

    &:hover {
      background-color: blue;
    }
  }

  &--primary {
    border-color: blueviolet;

    .card__title {
      font-size: 40px;
    }
  }
}
</style>

Other benefits – that are not really BEM related

Using a preprocessor has other benefits that are not directly tied to BEM, like variables, functions and mixins. These features make your CSS more modular, reusable, and maintainable. Mixins are very helpful when it comes to encapsulating common style patterns that many components might share. Here are some examples.

nuxt.config.ts
TypeScript
// This is how you would add your global styles in a Nuxt project.

export default defineNuxtConfig({
  css: ['~/assets/less/main.less'],
})
./assets/less/main.less
Less
// Example of a main.less file that handles global styles.

@import url(./reset.less);
@import url(./mixins.less);
@import url(./global.less);
@import url(./typographyless);
./assets/less/mixins.less
Less
// Examples of mixins that you could use in different places.

.sans-bold() {
  font-family: var(--sans-font-family);
  font-weight: 800;
  font-style: normal;
}

.card-style(@padding: 16px, @borderRadius: 8px, @shadowColor: rgba(0, 0, 0, 0.2)) {
  padding: @padding;
  border-radius: @borderRadius;
  box-shadow: 0 4px 8px @shadowColor;
  background-color: #fff;
  transition: box-shadow 0.3s ease;

  &:hover {
    box-shadow: 0 8px 16px @shadowColor;
  }

  @media (max-width: 576px) {
    padding: 12px;
    border-radius: 4px;
    box-shadow: 0 2px 4px @shadowColor;
  }

  @media (min-width: 577px) and (max-width: 768px) {
    padding: 14px;
    border-radius: 6px;
    box-shadow: 0 3px 6px @shadowColor;
  }

  @media (min-width: 769px) and (max-width: 1200px) {
    padding: 16px;
    border-radius: 8px;
    box-shadow: 0 4px 8px @shadowColor;
  }

  @media (min-width: 1201px) {
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 6px 12px @shadowColor;
  }
}
Vue
<template>
  <div class="card">
    <h2 class="card__title">Title</h2>
    <p class="card__content">Content</p>
    <button class="card__button">Click me</button>
  </div>
</template>

<style lang="less">
// I can reference my mixins file and use the mixins here.
@import (reference) '~/assets/less/mixins.less';

.card {
  .card-style();

  &__title {
    .sans-bold();
    font-size: 20px;
  }

  &__content {
    font-size: 16px;
  }

  // The rest of you styles...
}
</style>

The examples above are very basic but still show how you could extract whole blocks of CSS into mixins that can be re-used accross your codebase – both in your global styles as well as in individual components. If this is done well, it can really make global style changes quite easy.

Installing and using Less in your Nuxt project

Thanks to Vite, it is very straightforward to set Less up in a Nuxt project. Simply install it using npm install less (or using the package manager of your choice). That's it – now you can add a lang="less" to your style tag. This process should be the same for Sass.

Vue
<template>
  <div class="hero-banner">
    <h1 class="hero-banner__title">Title</h1>
  </div>
</template>

<style lang="less">
.hero-banner {
  background-color: red;
  padding: 8px 16px;

  &__title {
    font-size: 24px;
  }
}
</style>

Summary

As someone that really enjoys writing custom CSS, I think BEM is a great choice when structuring your projects CSS – especially now with front-end frameworks that use a component based architecture. I think it is easy to read and write and the fact that you don't need to install anything makes it very future proof – since you are just writing vanilla CSS in the end. I find the core ideas of BEM fundamentally solid and I think it is still a great choice.

I recommend you give BEM a try – let me know what you think!