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.
Resources:
- https://vue-loader.vuejs.org/guide/scoped-css.html
- https://github.com/vuejs/vue-loader/blob/master/lib/loaders/stylePostLoader.js#L12
- https://github.com/vuejs/vue-loader/blob/master/lib/loaders/templateLoader.js#L27
- https://github.com/vuejs/component-compiler-utils/blob/master/lib/stylePlugins/scoped.ts
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