Theming using CSS Custom Properties in Vue.js Components
Hello there! The topic of this VueDose tip is to create super generic, flexible and yet robust components. Ready? Let's go!
Let's start this tip with a simple button:
<template>
<button><slot/></button>
</template>
<script>
export default {
name: "AppButton",
};
</script>
<style scoped>
button {
border: 4px solid #41b883;
padding: 12px 24px;
transition: 0.25s ease-in-out all;
}
button:hover {
color: white;
background-color: #41b883;
}
</style>
We want a component to be as generic as possible, to the point that we could reuse it in different websites with different brands. But how can we do that?
Well, first of all, we are not going to assume that we'll always get text as content. What if we want to pass different markup like <strong></strong>
or pass an icon? The best way to overcome this problem? Make use of slots.
Let's work on the styling now. As always, a good rule of thumb is to use scoped which makes our CSS local. However, we want some things global, right? For instance the brand colors. Right now we are breaking the DRY principle by duplicating the concept of a primary color. If we needed to change the primary color we'll have to change it in every place it's hardcoded. The solution? Use custom properties.
<style scoped>
:root {
--primary-color: #41b883;
--on-primary-color: white;
--small-spacing: 12px;
--normal-spacing: calc(var(--small-spacing) * 2);
}
button {
border: 4px solid var(--primary-color);
padding: var(--small-spacing) var(--normal-spacing);
transition: 0.25s ease-in-out all;
}
button:hover {
color: var(--on-primary-color);
background-color: var(--primary-color);
}
</style>
We would normally define custom properties in another file and target the :root
selector and not inside the component, but for the sake of this example we are going to do it inside the component.
Colors and spacing is usually what changes the most in web design, so we have to make sure we protect ourselves from these changes.
This makes the component very flexible as we can change these properties and it will affect all the elements that make use of them.
However, how could we tackle theming? Well we could use non-declared custom properties. Wait. What? Let me explain with code:
<style scoped>
button {
border: 4px solid var(--button-border-color, var(--primary-color));
padding: var(--small-spacing) var(--normal-spacing);
transition: 0.25s ease-in-out all;
}
button:hover {
color: var(--button-hover-text-color, var(--on-primary-color));
background-color: var(--button-hover-background-color, var(--primary-color));
}
</style>
Where are we declaring --button-border-color
, --button-hover-text-color
and --button-hover-background-color
? That's the trick, we are not.
We are using an undefined custom property but giving it a default value. So, if one of these properties does not exist at runtime it will fallback to the default value.
This means that from the outside we can do something as follows:
<template>
<AppButton class="custom-theme">Hello VueDose!</AppButton>
</template>
<style scoped>
.custom-theme {
--button-border-color: pink;
--button-hover-background-color: rgb(206, 161, 195);
}
</style>
And this is super flexible, but maybe too flexible. We don't want to expose so much knowledge to the client. Maybe the client just wants to set instead of a primary colored button a secondary theme. And the button should be able to know what things it has to set in order to have the look and feel of a secondary button. So let's mix custom properties and themes!
<template>
<button :style="getTheme"><slot/></button>
</template>
<script>
export default {
name: "AppButton6",
props: {
theme: String,
validator: (theme) => ['primary', 'secondary'].includes(theme)
},
computed: {
getTheme() {
const createButtonTheme = ({
borderColor,
hoverTextColor,
hoverBackgroundColor
}) => ({
'--button-border-color': borderColor,
'--button-hover-text-color': hoverTextColor,
'--button-hover-background-color': hoverBackgroundColor
})
const primary = createButtonTheme({
borderColor: 'var(--primary-color)',
hoverTextColor: 'var(--on-primary-color)',
hoverBackgroundColor: 'var(--primary-color)'
})
const secondary = createButtonTheme({
borderColor: 'var(--secondary-color)',
hoverTextColor: 'var(--on-secondary-color)',
hoverBackgroundColor: 'var(--secondary-color)'
})
const themes = {
primary,
secondary
}
return themes[this.theme]
}
}
};
</script>
So we can do this easily:
<AppButton theme="secondary">Hello VueDose!</AppButton>
And finally. All the cool kids nowadays are doing dark themes right?
<template>
<main class="wrapper" :class="mode">
<AppButton @click.native="toggleTheme" theme="secondary">
Click me to change to dark mode!
</AppButton>
</main>
</template>
<script>
import AppButton from "./components/AppButton";
export default {
name: "App",
data: () => ({
mode: 'light'
}),
components: {
AppButton
},
methods: {
toggleTheme() {
this.mode = this.mode === 'light' ? 'dark' : 'light'
}
}
};
</script>
<style scoped>
.light {
--background-color: white;
--on-background-color: #222;
}
.dark {
--background-color: #222;
--on-background-color: white;
}
.wrapper {
transition: 1s ease-in-out background-color;
background-color: var(--background-color);
color: var(--on-background-color);
}
</style>
We can switch --background-color
and --on-background-color
values to create new themes! And that will be all. Thanks for reading through all!
Just like that, it works! Check it out yourself in this CodeSandbox!
Here it goes today's tip!
Related Articles
Creating UI components based on a Design System in Vue.js
Don't you know the benefits of using a Design System? Not sure how to structure your Vue.js components in an app? Read the article and find it out.
Jul 21, 2020
How to structure a Vue.js app using Atomic Design and TailwindCSS
Unsure about how to organize your Vue.js components? Learn in this tutorial how to do it using Atomic Design methodology. Examples and demos included!
Jun 16, 2020
The most modern Pie Chart component using CSS Conic Gradient and Vue.js
Build a Pie Chart component using one of the modern CSS features Conic Gradient
Oct 21, 2019
Sponsors
VueDose is proudly supported by its sponsors. If you enjoy it, consider supporting it to ensure the project maintainability.