CSS in vue components

May 2020 - Stéphane Reiss

All about <style>

Who am I?

  • Web developer
  • PHP background
  • I love javascript

Stéphane Reiss

<style>
...
</style>
<style scoped>
...
</style>
<style module>
...
</style>
<style lang="scss">
...
</style>
<style lang="less">
...
</style>

Agenda

1. CSS in SFCs

2. Scoped styles

3. Module styles

4. Pre Processors

<style lang="postcss">
...
</style>
<style lang="stylus">
...
</style>
<style lang="scss">
...
</style>

Single File Components

<template>
  <div>
    <header class="header">
      <div class="nav">
        <div>Link 1</div>
        <div>Link 2</div>
        <div>Link 3</div>
      </div>
    </header>
    <main>
      <!-- CONTENT -->
    </main>
  </div>
</template>

<style>
  body {
    margin: 0;
    background-color: #FFF;
    font-family: Arial;
  }
  .nav {
    background-color: #282C34;
    height: 40px;
  }
  .nav div{
    display: inline-block;
    margin: 0 10px;
    padding: 5px;
    color: #FFF;
  }
</style>

app.vue

<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue CLI App</title>

    <style type="text/css">
      body {
        margin: 0;
        background-color: #FFF;
        font-family: Arial;
      }
      .nav {
        background-color: #282C34;
        height: 40px;
      }
      .nav div{
        display: inline-block;
        margin: 0 10px;
        padding: 5px;
        color: #FFF;
      }
    </style>
  </head>
  <body>
    <div>
      // TEMPLATE RENDERING
    </div>
    <script type="text/javascript" src="/app.js"></script>
  </body>
</html>

vue-style-loader transform your CSS into a <style> tag and adds it to the DOM.
See addStylesClient.js

<template>
  <div>
    <div class="nav">
      <div>Tab 1</div>
      <div>Tab 2</div>
      <div>Tab 3</div>
    </div>
    <div class="content">
      <h1>LOREM IPSUM</h1>
      <p>Sed maximus ...</p>
    </div>
  </div>
</template>

<style>
  .nav {
    float: left;
    border: 1px solid #282c34;
    border-radius: 6px;
    margin-right: 10px;
  }

  .nav div {
    margin: 0 10px;
    padding: 5px;
    border-bottom: 1px solid #282c34;
  }

  .nav div:last-child {
    border-bottom: none;
  }

  .content {
    float: left;
    min-height: 200px;
    background-color: #282c34;
    border-radius: 6px;
    padding: 20px;
    width: 250px;
  }
  .content h1 {
    color: #7ec699;
  }
  .content p {
    color: #f8c555;
  }
</style>

content.vue

Let's create another component:

And import it in our app

<script>
import Content from './content.vue'

export default {
  components: {
    Content,
  },
}
</script>

<template>
  <div>
    <header class="header">
      <div class="nav">
        <div>Link 1</div>
        <div>Link 2</div>
        <div>Link 3</div>
      </div>
    </header>
    <main>
      <Content />
    </main>
  </div>
</template>

<style>
  // ...
</style>

app.vue

It now renders this:

<head>
    <meta charset="utf-8">
    <title>Vue CLI App</title>

    <style type="text/css">
      .nav {
        float: left;
        border: 1px solid #282c34;
        border-radius: 6px;
        margin-right: 10px;
      }
      .nav div {
        margin: 0 10px;
        padding: 5px;
        border-bottom: 1px solid #282c34;
      }

      // ...
    </style>
    <style type="text/css">
      // ...
      .nav {
        background-color: #282C34;
        height: 40px;
      }
      .nav div{
        display: inline-block;
        margin: 0 10px;
        padding: 5px;
        color: #FFF;
      }
      // ...
    </style>
</head>

WITH SCOPED STYLE

Scoped style

<template>
  ...
</template>

<style scoped>
  body {
    margin: 0;
    background-color: #FFF;
    font-family: Arial;
  }

  .nav {
    background-color: #282C34;
    height: 40px;
  }

  .nav div{
    display: inline-block;
    margin: 0 10px;
    padding: 5px;
    color: #FFF;
  }
</style>

app.vue

<template>
  ...
</template>

<style scoped>
  .nav {
    float: left;
    border: 1px solid #282c34;
    border-radius: 6px;
    margin-right: 10px;
  }

  .nav div {
    margin: 0 10px;
    padding: 5px;
    border-bottom: 1px solid #282c34;
  }

  .nav div:last-child {
    border-bottom: none;
  }

  .content {
    float: left;
    min-height: 200px;
    background-color: #282c34;
    border-radius: 6px;
    padding: 20px;
    width: 250px;
  }
  .content h1 {
    color: #7ec699;
  }
  .content p {
    color: #f8c555;
  }
</style>

content.vue

Now app renders like this:

FIXED

almost ...
let's look at the DOM

<head>
  <meta charset="utf-8">
  <title>Vue CLI App</title>

  <style type="text/css">
    .nav[data-v-62d680db] {
      float: left;
      border: 1px solid #282c34;
      border-radius: 6px;
      margin-right: 10px;
    }
    .nav div[data-v-62d680db] {
      margin: 0 10px;
      padding: 5px;
      border-bottom: 1px solid #282c34;
    }
    ...
  </style>

  <style type="text/css">
    body[data-v-381730fa] {
      margin: 0;
      background-color: #FFF;
      font-family: Arial;
    }
    .nav[data-v-381730fa] {
      background-color: #282C34;
      height: 40px;
    }
    ...
  </style>
</head>
<body>
  <div data-v-381730fa="">
    <header data-v-381730fa="" class="header">
      <div data-v-381730fa="" class="nav">
        <div data-v-381730fa="">Link 1</div>
        <div data-v-381730fa="">Link 2</div>
        <div data-v-381730fa="">Link 3</div>
      </div>
    </header>
    <main data-v-381730fa="">
      <div data-v-62d680db="" data-v-381730fa="">
        <div data-v-62d680db="" class="nav">
          <div data-v-62d680db="">Tab 1</div>
          <div data-v-62d680db="">Tab 2</div>
          <div data-v-62d680db="">Tab 3</div>
        </div>
        <div data-v-62d680db="" class="content">
          <h1 data-v-62d680db="">LOREM IPSUM</h1>
          <p data-v-62d680db="">Sed maximus ...</p>
        </div>
      </div>
    </main>
  </div>

  <script type="text/javascript" src="/app.js"></script>

</body>

When a <style> tag has the scoped attribute, its CSS will apply to elements of the current component only.

This is done by vue-loader, generating a unique hash per component (based on the file name), using the package hash-sum

Then added to the DOM element (div, script, etc..) as part of the compilation.

But wait, we haven't fixed the problem...

You can mix scoped and non-scoped style in the same component!

<template>
  ...
</template>

<style>
  body {
    margin: 0;
    background-color: #FFF;
    font-family: Arial;
  }
</style>

<style scoped>
  .nav {
    background-color: #282C34;
    height: 40px;
  }

  .nav div{
    display: inline-block;
    margin: 0 10px;
    padding: 5px;
    color: #FFF;
  }
</style>

app.vue

<head>
  <meta charset="utf-8">
  <title>Vue CLI App</title>

  <style type="text/css">
    .nav[data-v-62d680db] {
      float: left;
      border: 1px solid #282c34;
      border-radius: 6px;
      margin-right: 10px;
    }
    ...
  </style>

  <style type="text/css">
    body {
      margin: 0;
      background-color: #FFF;
      font-family: Arial;
    }
  </style>

  <style type="text/css">
    .nav[data-v-381730fa] {
      background-color: #282C34;
      height: 40px;
    }
    ...
  </style>
</head>

FIXED

For good!

Scoped style

and the deep selector

You sometime need to style a child component.

Example:

<template>
  ...
</template>

<style scoped>
  ...
  main h1 {
    border: 1px solid #ee6004;
    border-radius: 5px;
    text-align: center;
  }
  ...
</style>

app.vue

<head>
  <meta charset="utf-8">
  <title>Vue CLI App</title>

  <style type="text/css">
    ...
    main h1[data-v-5ef48958] {
      border: 1px solid #ee6004;
      border-radius: 5px;
      text-align: center;
    }
    ...
  </style>
</head>
<template>
  ...
</template>

<style scoped>
  ...
  main >>> h1 {
    border: 1px solid #ee6004;
    border-radius: 5px;
    text-align: center;
  }
  ...
</style>

app.vue

<head>
  <meta charset="utf-8">
  <title>Vue CLI App</title>

  <style type="text/css">
    ...
    main[data-v-5ef48958]  h1 {
      border: 1px solid #ee6004;
      border-radius: 5px;
      text-align: center;
    }
    ...
  </style>
</head>

Some pre-processors, such as Sass, may not be able to parse >>> properly.
In those cases you can use the /deep/ or ::v-deep combinator instead:

main >>> h1 { ... }
main /deep/ h1 { ... }
main ::v-deep h1 { ... }

Module style

<template>
  ...
</template>

<style module>
  .nav {
    float: left;
    border: 1px solid #282c34;
    border-radius: 6px;
    margin-right: 10px;
  }
  
  ...
</style>

content.vue

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

Must be enabled by passing modules: true to css-loader

<style type="text/css">
  .content_nav_xgpl- {
    float: left;
    border: 1px solid #282c34;
    border-radius: 6px;
    margin-right: 10px;
  }
  .content_nav_xgpl- div {
    margin: 0 10px;
    padding: 5px;
    border-bottom: 1px solid #282c34;
  }
  .content_nav_xgpl- div:last-child {
    border-bottom: none;
  }
  .content_content__WyWO {
    float: left;
    min-height: 200px;
    background-color: #282c34;
    border-radius: 6px;
    padding: 20px;
    width: 250px;
  }
  .content_content__WyWO h1 {
    color: #7ec699;
  }
  .content_content__WyWO p {
    color: #f8c555;
  }
</style>

Also know as CSS-in-JS

The module attribute instructs Vue Loader to inject the CSS modules locals object into the component as a computed property with the name $style.

You can then use it in your templates with a dynamic class binding:

<template>
  <div>
    <div :class="$style.nav">
      <div>Tab 1</div>
      <div>Tab 2</div>
      <div>Tab 3</div>
    </div>
    <div :class="$style.content">
      <h1>LOREM IPSUM</h1>
      <p>Sed maximus ...</p>
    </div>
  </div>
</template>

<style module>
.nav { ... }
.content { ... }
</style>

content.vue

<div data-v-381730fa="">
  <div class="content_nav_xgpl-">
    <div>Tab 1</div>
    <div>Tab 2</div>
    <div>Tab 3</div>
  </div>
  <div class="content_content__WyWO">
    <h1>LOREM IPSUM</h1>
    <p>Sed maximus ...</p>
  </div>
</div>

Pre processors

content.vue

<style scoped lang="scss">
  .nav {
    // ...
    div {
      margin: 0 10px;
      padding: 5px;
      border-bottom: 1px solid #282c34;

      &:last-child {
        border-bottom: none;
      }
    }
  }

  .content {
    // ...
    h1 {
      color: #7ec699;
    }

    p {
      color: #f8c555;
    }
  }
</style>
$ npm install -D sass-loader node-sass
module.exports = {
  module: {
    rules: [
      // ...

      // this will apply to both plain `.scss` files
      // AND `<style lang="scss">` blocks in `.vue` files
      {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  // ...
}

1. Install your pre-processor

2. Configure webpack to use it

3. Use it in your component

Tips & tricks

const path = require("path");
module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        css: path.join(__dirname, "src/very/deep/folder")
      }
    }
  }
};

Different ways to import your CSS:

Use aliases in CSS:

// Inline
<style>
  body { ... }
</style>

// File import
<style src="./style.css"></style>

// CSS import
<style>
  @import './style.css';
</style>

vue.config.js

<style>
  @import "~css/main.css";
</style>
<style>
  @import "../../very/deep/folder/main.css";
</style>
<template>
  <p :class="custom.red">
    ...
  </p>
</template>

<style module="custom">
  .red {
    color: red;
  }
</style>

Custom identifier for CSS modules (instead of $style)

Thank you!

CSS in vue components

By Stéphane Reiss

CSS in vue components

  • 674