Today’s tip comes from Cesar Alberca (@cesalberca), a frontend developer at Autentia that have worked with Vue professionally for 2 years and counting. He’s also done projects in React and Angular and he’s passionate about good practices and testing.

I met Cesar at the last Codemotion Madrid conference and I have to say he’s a really cool guy with lots of potential! Don’t expect too little from this tip 😉.

Let’s jump into Cesar’s tip!


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!

Remember you can read this tip online (with copy/pasteable code), and don’t forget to share VueDose with your colleagues, so they also know about these tips as well!

See you soon.

Start saving time and get a tip about the Vue ecosystem every week, right in your inbox.

    Latest Vue Jobs