Theming using CSS Custom Properties in Vue.js Components

César Alberca

César Alberca

Nov 5, 2019
4 min read
Share on Twitter or LinkedIn

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!

Don't miss out anything about Vue, your favourite framework.

Subscribe to receive all the articles we publish in a concise format, perfect for busy devs.

Related Articles

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!

Alba Silvente

Alba Silvente

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

Alex Jover Morales

Alex Jover Morales

Oct 21, 2019

The most modern Carousel component using CSS Scroll Snap and Vue.js

Build a Carousel by using one of the latest CSS features called scroll-snap and bundle it into a Vue.js component

Alex Jover Morales

Alex Jover Morales

Oct 6, 2019

Sponsors

VueDose is proudly supported by its sponsors. If you enjoy it, consider supporting it to ensure the project maintainability.

Silver
StoryBlok
Learning Partner
Vue School