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>
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>
Alternatively, we can supply url in src attribute for external style.
<style src="path/to/external/style">...</style>
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>
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>
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>
Above selector has a higher specificity than attribute selector in the child component.
<style scoped>
.some-child-class[data-v-xxxxxx] {
color: blue;
}
</style>
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>
will compile into the following:
<style scoped>
.some-selector[data-v-xxxxxx] .some-child-class { /* ... */ }
</style>
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>
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>
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>
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>
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>
Well done! our list component now have a default and customizable style features.
Top comments (0)