Skip to content

Component libraries and CSS

So far every component of the course has brought its own CSS. In single file components, Vue offers several ways to limit styles to one component. If you'd rather not design forms, tables, menus and dialogs yourself, you can reach for a library of ready-made components or for a CSS framework.

A library saves time on elements that are hard to build well, such as a date field, a sortable table or an accessible menu. In return it brings its own look, extra size in the build and a dependency you will hardly get rid of later. Before you choose one, check that it fits your version of Vue, that it is maintained and that it describes how it handles the keyboard and screen readers. For a small project, your own CSS with scoped is often enough.

With <style scoped>, Vue gives every element of the component an attribute of its own and adds it to the selectors. The rules then only apply inside this component. The outermost element of a child component also gets the attribute of its parent, which is why .card .price reaches the price. An element deeper inside the child can only be reached with :deep(). The paragraph below the card has the same class price but stays plain, because it is not inside the card.

App.vue

App.vue
<script setup>
import PriceTag from './PriceTag.vue'
</script>
<template>
<article class="card">
<h2>Drill</h2>
<PriceTag :deposit="20" />
</article>
<p class="price">This paragraph is not part of the card and stays plain.</p>
</template>
<style scoped>
.card {
max-width: 16rem;
padding: 0.75rem;
border: 1px solid #c8c8c8;
border-radius: 0.5rem;
}
h2 {
margin: 0 0 0.5rem;
}
/* The root element of a child also carries the attribute of this component, so this rule reaches it */
.card .price {
font-weight: 700;
}
/* An element inside the child is only reached through :deep() */
.card :deep(.currency) {
color: #2e7d32;
}
</style>

PriceTag.vue

PriceTag.vue
<script setup>
defineProps({
deposit: { type: Number, required: true },
})
</script>
<template>
<span class="price"><span class="currency">CHF</span> {{ deposit }} deposit</span>
</template>

v-bind() in CSS ties a value to the state of the component. Vue sets it as a CSS variable and changes it as soon as the state changes.

App.vue
<script setup>
import { computed, ref } from 'vue'
const available = ref(true)
const color = computed(() => (available.value ? '#2e7d32' : '#b71c1c'))
</script>
<template>
<p class="badge">{{ available ? 'In' : 'Out' }}</p>
<button type="button" @click="available = !available">Switch the status</button>
</template>
<style scoped>
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 1rem;
color: #fff;
/* v-bind() ties the colour to the reactive value and updates it */
background: v-bind(color);
}
</style>

<style module> turns every class into a unique name that the template gets through $style. The playground does not support CSS modules, but in a project with Vite the component works as shown.

StatusBadge.vue
<script setup>
defineProps({
available: { type: Boolean, required: true },
})
</script>
<template>
<span :class="[$style.badge, available ? $style.in : $style.out]">{{ available ? 'In' : 'Out' }}</span>
</template>
<style module>
/* The module turns every class into a unique name, $style returns it by its name in the CSS */
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 1rem;
color: #fff;
}
.in {
background: #2e7d32;
}
.out {
background: #b71c1c;
}
</style>

The well-known libraries for Vue 3 differ mostly in their look. Vuetify follows Material Design, Quasar brings more than 120 components and its own build tools, and Element Plus and Naive UI have designs of their own. Reka UI, formerly Radix Vue, ships components without styles that only bring behavior and accessibility. These libraries are under the MIT license. Since version 5, PrimeVue needs a license key and comes under a license of its own, PrimeVue 4 is still under MIT. Read the license and the page on accessibility before you choose, not every library has one.

Tailwind CSS takes a different path. Instead of ready-made components, you write small classes such as flex or p-4 straight into the template. With Vite you add it through the plugin @tailwindcss/vite and the line @import "tailwindcss"; in the CSS. Tailwind looks for the classes as plain text in your files, so a class name that the code puts together at run time gets no CSS. Tailwind also removes the bullets of lists, and VoiceOver then no longer announces such lists as lists unless they carry role="list".

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.