DEV Community

jerry 🐭
jerry 🐭

Posted on • Edited on

Vue 2 Component Default and Customizable Style

Live Demo

If you're planning to create your own vue component library (for yourself and/or for others) or you want to implement having a default yet customizable style at the same time for your existing vue library, then you'll find it helpful.

Say we're going to build a very simple list component.

<template>
  <div>
    <ul v-if="items.length">
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>

    <p v-else>No items to show</p>
  </div>
</template>

<script>
export default {
  name: "ListItem",
  props: {
    items: {
      type: Array,
      default: () => [];
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

Default Style

To have a default styling, we just have to define component specific classes inside style tag in our component.

<template>
  <div class="list-container">
    <ul class="list" v-if="items.length">
      <li class="list__item" v-for="item in items" :key="item">
        {{ item }}
      </li>
    </ul>

    <p v-else>No items to show</p>
  </div>
</template>

<script>...</script>

<style>
.list-container {
  padding: 0.5rem 1rem;
  background: #ef9a9a;
  color: #232429;
  line-height: 180%;
  font-size: 0.875rem;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.list__item {
  background: #e8eaf6;
  padding: 0.25rem;
  margin: 0.5rem;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can supply url in src attribute for external style.

<style src="path/to/external/style">...</style>
Enter fullscreen mode Exit fullscreen mode

Scoped Style

We dont want our component style accidentally affects other style but ours, we need some scoping.

Scoped style restrict our component specific styles to itself, and I prefer building vue component this way to prevent selector clashing or conflicts.

Unless you intend to affect elements aside your own component.

<style scoped>...</style>
Enter fullscreen mode Exit fullscreen mode

Great!, our list component now have default styling.

Custom Style

Now that we have default styles implemented, it's time to make our list component style customizable.

We can define class from parent and will override child classes right?

No this will not work, unfortunately.

All thanks because of scoped style attribute selector .some-class[data-v-xxxxxx] it has a higher css specificity.

Understanding Scoped Style

Vue scoped style dynamically add a data attribute on elements in its template and use that data attribute for css attribute selector, doing so will give a component specific css selectors a higher specificity.

Scoped style limits component's own style to itself and prevents parent styles to modify childs.

<style scoped>
.example {
 color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

Will compile into the following:

<style scoped>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

Scoped Style

What is CSS Specificity?

Specificity is the measurement of relevance that determines which style rule will applied to an element if there are two or more rules points to the same element.

Overriding Default Style

Knowing what css specificity is and how scope style works, we just need to ensure our custom styles has a higher specificity right?

Indeed!, we can override child scoped style like this:

<style>
.some-parent-class .some-child-class {
  color: red;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Above selector has a higher specificity than attribute selector in the child component.

<style scoped>
.some-child-class[data-v-xxxxxx] {
  color: blue;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Therefore it'll be applied.

Deep Selector

Vue also has a better solution to this, a deep selector using a >>> combinator.

<style scoped>
.some-selector >>> .some-child-class { /* ... */ }
</style>
Enter fullscreen mode Exit fullscreen mode

will compile into the following:

<style scoped>
.some-selector[data-v-xxxxxx] .some-child-class { /* ... */ }
</style>
Enter fullscreen mode Exit fullscreen mode

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 - both are aliases for >>> and work exactly the same.

<style scoped>
.a::v-deep .b { /* ... */ }
</style>

or

<style scoped>
.a /deep/ .b { /* ... */ }
</style>

Deep Selectors

That's a great way of customizing default styles by overriding childs, however it is not scalable.

If we ever use a third party styles or css frameworks or styles that we have no control over, we can't override child styles.

Using a Prop

Okay so overriding style is not what we want, instead we're going to bind custom classes in our list component elements, and assign our list component style as prop's default.

In order to do that we need props option to pass down custom classes.

<template>
  <div :class="listClasses.listContainer">
    <ul :class="listClasses.list" v-if="items.length">
      <li
        :class="listClasses.listItem"
        v-for="item in items"
        :key="item">
        {{ item }}
      </li>
    </ul>
    ...
  </div>
</template>

<script>
export default {
  props: {
    ...
    listClasses: {
      type: Object,
      default() {
        listContainer: "list-container",
        list: "list",
        listItem: "list__item"
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

I'm going to define listClasses prop as an object to target multiple elements in one declaration.

As a side note, you can use String, Array, Object as a class prop type.

  • String - It is intended for plain classes and pointing at a single element, You can pass a single or multiple space seperated classes "class-a class-b".
  • Array - It is intended for plain and conditional classes pointing at a single element ["class-a", {"class-b": true}].
  • Object - It is intended for a more complex classes pointing at multiple elements. {"classA": "class-a", classB: {"class-b": true}, classC: ["classC", {"classD": true}]}

This will now work, however passing listClasses prop will override default value and limit us to use one stylesheet at a time.

Its perfectly fine that way, but we can offer more flexibility.

Computed Property

Sometimes we want to modify default style partially and merge the rest of the component's style declaration.

That's where computed property comes in, we can derive listClasses prop to still provide a default value if not supplied.

What's more is we can now merge default classes if prop is partially defined.

<template>
  <div :class="obtainClasses.listContainer">
    <ul :class="obtainClasses.list" v-if="items.length">
      <li
        :class="obtainClasses.listItem"
        v-for="item in items"
        :key="item">
        {{ item }}
      </li>
    </ul>
    ...
  </div>
</template>

<script>
export default {
  props: {
    ...
    listClasses: {
      type: Object,
      default: () => ({})
    }
  },
  computed: {
    obtainClasses() {
      const defaultClasses = {
        listContainer: "list-container",
        list: "list",
        listItem: "list__item"
      };

      return Object.assign(defaultClasses, this.listClasses);
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

What we're doing here is we prioritize prop classes(custom class) and have our default class as a fallback.

Nice-to-Haves

We've done a great progress in our list component but we still have to offer.

Additional Configuration

We can implement mergeDefault prop configuration, that determines whether we want to merge a default class if listClasses prop is partially supplied or not.

<script>
export default {
  props: {
    ...
    mergeDefault: {
      type: Boolean,
      default: true
    }
  },
  computed: {
    obtainClasses() {
      const defaultClasses = {
        listContainer: "list-container",
        list: "list",
        listItem: "list__item"
      };

      if (this.mergeDefault)
        return Object.assign(defaultClasses, this.listClasses);
      return Object.keys(this.listClasses).length ?
        this.listClasses : defaultClasses;
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Finishing Touch

The class name you're going to pass shoudn't match the class of child component you're going to customize.

Since we didn't override default classes instead we're prioritizing custom class over default.

Passing class of the same name as the childs is like you did nothing aside from providing aditional css declaration if any.

For additional measure, we can implement a unique naming class inside our component.

<script>
export default {
  ...
  computed: {
    obtainClasses() {
      const defaultClasses = {
        listContainer: "_list-container",
        list: "_list",
        listItem: "_list__item"
      };
      ...
    }
}
</script>

<style scoped>
/* here we name our classes with underscore in the beginning */
._list-container { /* */ }

._list { /* */ }

._list__item { /* */ }
</style>
Enter fullscreen mode Exit fullscreen mode

Well done! our list component now have a default and customizable style features.

Top comments (0)