VueJS

Styling Vue Single-File Components

Lindsay Wardell
4 min read
Styling Vue Single-File Components
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Introduction

If you have any experience with writing Vue single-file components, you have probably spent some time writing CSS within your component. Single-File Components allow developers to group code together in more logical ways, rather than breaking up components by language utilized (HTML, CSS, or JavaScript). Being able to group component styles directly next to the HTML that it applies to is one of the major benefits of Vue, including the ability to scope CSS to the component so that it doesn't affect other parts of the UI.

However, there are a number of features to Vue's CSS integration that you may not be familar with, such as applying styles directly to slotted elements, or the newest features available in Vue 3.2. Let's explore some of these other ways of styling Vue single-file components, and how they can benefit your applications.

Scoped Styles

Let's start with the most common usage of CSS in Vue: scoped styles. One of the difficulties on writing modern applications is that our CSS files begin to grow larger and larger, until nobody really knows where certain styles are used or what a given change might affect. This can lead to copying certain CSS selectors, and simply duplicating them for each component. There are other solutions for this (such as BEM or utility classes), but when working with a component-based framework like Vue, it makes a lot of sense to group CSS classes within the component.

Scoped styles allows us to write CSS that only applies to the component we are working in. Here's an example from the Vue docs:

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

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

With this, the example class will only ever apply within this component. This is achieved by added a unique data attribute to all elements within the component, so the normal CSS cascade is still applied. External styles can still impact the design of this component, but its scoped styles cannot leak out to other components.

Deep Styles

This leads to an interesting problem. If our component's styles are scoped, what about children components? By default, they would not receive any styling from our scoped styles. However, Vue provides a way to do that. Let's look at an example below.

<!-- Card.vue -->
<template>
  <div>
    <header>
      <Title>
        <slot name="title">Card Title</slot>
      </Title>
    </header>
    <section>
      <slot>Lorum ipsum dolor sit amet</slot>
    </section>
  </div>
</template>

<style scoped>
header :deep(.card-title) {
  font-weight: bold;
}

section {
  padding 2rem;
}
</style>

<!-- Title.vue -->
<template>
  <div class="card-title"><slot>Title</slot></div>
</template>

By using the :deep() pseudo class, we are able to tell Vue that this particular class (.card-title) should not be scoped. Because the special ID is still applied to the root element (header), the style is still scoped, but it is available for any child component beneath it.

Slotted Styles

A problem I've run into in many situations is that I have a component being injected with slots, but I cannot control the styling of it like I want. Vue provides a solution for this as well with slotted styles. Let's review the above example, but this time we'll add a slotted style to our Title.vue component.

<!-- Card.vue -->
<template>
  <div>
    <header>
      <Title>
        <slot name="title">Card Title</slot>
      </Title>
    </header>
    <section>
      <slot>Lorum ipsum dolor sit amet</slot>
    </section>
  </div>
</template>

<style scoped>
header :deep(.card-title) {
  font-weight: bold;
}

section {
  padding 2rem;
}
</style>

<!-- Title.vue -->
<template>
  <div class="card-title">
    <slot>Title</slot>
  </div>
</template>

<style scoped>
:slotted(h1) {
  font-size: 3rem;
}
</style>

Here, we added the :slotted pseudo class, so that any slotted h1 tags have the correct style applied to them. This may be a contrived example, but consider needing to have different header styles for each header tag (or equivalent CSS class). The Title.vue component can manage all of these styles, rather than relying on the consumer of these components to pass in the correct class or styling.

Global Styles

Of course, sometimes you need to apply styles globally, even within a scoped component. Vue provides us with two different ways to handle this: the :global pseudo selector and multiple style blocks.

:global

Within a scoped style block, if you only need to provide one class as a global value, you can use the :global pseudo selector to note that the style should not be scoped. From the Vue docs:

<style scoped>
:global(.red) {
  color: red;
}
</style>

Multiple style blocks

There is also nothing stopping you from having multiple style blocks within your Vue component. Simply create another <style> tag, and put your global styles in there.

<style>
/* global styles */
</style>

<style scoped>
/* local styles */
</style>

Style Modules

If you're coming from React, you may be more familiar with CSS modules, where you import a CSS file and access its classes as a JavaScript object. The same can be done within Vue by using <style module> instead of <style scoped>. Here's an example from the Vue docs:

<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>

<style module>
.red {
  color: red;
}
</style>

This can be particularly nice to work with, so that you aren't passing strings around in your classes (which are prone to errors and typos). Vue also allows you to rename what the object is, so that you don't need to access them with $style in your template if you don't want to.

Dynamic CSS Values

The latest feature in Vue is state-driven dynamic CSS values. There is a trend in modern CSS to use custom properties as a way to dynamically update the value of some CSS property. This can allow our CSS to be more flexible, and interact nicely with our other application code. Let's look at an example component that renders a progress bar:

<template>
	<div>
		<strong>
			Progress
		</strong>
		<div>{{ progress }}%</div>
		<div class="progress-bar">
			<div></div>
		</div>
	</div>
</template>

<script setup>
import { watch } from 'vue'

const props = defineProps({
  progress: {
    type: Number,
    required: true
  }
})

watch(props.progress,
  (value) => 
    document
      .documentElement
      .style
      .setProperty('--complete-percentage', value + '%'),
      {
        immediate: true
      })
</script>

<style scoped>
.progress-bar {
	background-color: #ccc;
	border-radius: 13px;
	padding: 3px;
}

.progress-bar > div {
  background-color: #000;
  width: var(--complete-percentage);
  height: 8px;
  border-radius: 10px;
  transition-property: width;
  transition-duration: 150ms;
}
</style>

This component takes in a number (progress), then both displays that number and updates a CSS custom property with the value. As the progress changes, the CSS property is continually updated to stay in sync with the JavaScript value.

In Vue 3.2, however, we are provided with a special CSS function that does this whole thing for us! Take a look at the updated code:

<template>
	<div>
		<strong>
			Progress
		</strong>
		<div>{{ progress }}%</div>
		<div class="progress-bar">
			<div></div>
		</div>
	</div>
</template>

<script setup>
const props = defineProps({
  progress: {
    type: Number,
    required: true
  }
})
</script>

<style scoped>
.progress-bar {
	background-color: #ccc;
	border-radius: 13px;
	padding: 3px;
}

.progress-bar > div {
  background-color: #000;
  width:  v-bind(props.progress);
  height: 8px;
  border-radius: 10px;
  transition-property: width;
  transition-duration: 150ms;
}
</style>

By using v-bind(props.progress), we have elimited the need for our custom watcher, and it's clear that our CSS is being kept in sync with the value of props.progress. Under the hood, Vue is doing the same thing for us with a custom property, but it's so much nicer than having to write it ourselves.

Conclusion

CSS is a complicated language in practice, and mixing it with JavaScript makes things even more complex. Vue provides developers with the tools to handle CSS in a reliable and predictable way, which encourages building in a component-based architecture. Next time you're running into trouble with CSS in Vue, see if one of these techniques can be useful to you!

About the author

Lindsay Wardell

Lindsay Wardell

Software Engineer

Lindsay is a software engineer at This Dot Labs, and a host on the podcast Views on Vue. She loves learning about new languages and tools, and sharing that knowledge with the community.

Keep reading

View all posts →

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Unleash the power of 3D in your Vue.js projects with Tres.js! The future of immersive web experiences is here. #Vuejs #3Ddevelopment...

Mattia Magi5 mins
Vue3D

3 VueJS Component Libraries Perfect for Beginners

For developers checking out VueJS for the first time, the initial steps are overwhelming, particularly when setting up projects from square one. But don’t worry! The VueJS ecosystem offers a plethora of remarkable component libraries, easing this early obstacle. These three libraries are pre built toolkits, providing beginners with the means to kickstart their VueJS projects effortlessly. Let’s take a look! Quasar Quasar is among the most popular open source component libraries for Vue.js, offering a comprehensive set of ready to use UI components and tools for building responsive web applications and websites. Designed with performance, flexibility, and ease of use in mind, Quasar provides developers with a wide range of customizable components, such as buttons, forms, dialogs, and layouts, along with built in support for themes, internationalization, and accessibility. With its extensive documentation, active community support, and seamless integration with Vue CLI and Vuex, Quasar empowers developers to rapidly prototype and develop high quality Vue.js applications for various platforms, including desktop, mobile, and PWA (Progressive Web Apps). PrimeVue PrimeVue is a popular Vue.js component library offering a wide range of customizable UI components designed for modern web applications. Developed by PrimeTek, it follows Material Design guidelines, ensuring responsiveness and accessibility across devices. With features like theming, internationalization, and advanced functionalities such as lazy loading and drag and drop, PrimeVue provides developers with the tools to create elegant and high performing Vue.js applications efficiently. Supported by clear documentation, demos, and an active community, PrimeVue is an excellent choice for developers seeking to streamline their development process and deliver polished user experiences. Vuetify Vuetify is a powerful Vue.js component library that empowers developers to create elegant and responsive user interfaces with ease. Built according to Google's Material Design guidelines, Vuetify offers a vast collection of customizable UI components, ranging from buttons and cards to navigation bars and data tables. Its comprehensive set of features includes themes, typography, layout grids, and advanced components like dialogues and sliders, enabling developers to quickly build modern web applications that look and feel polished. With extensive documentation, active community support, and ongoing development, Vuetify remains a top choice for Vue.js developers seeking to streamline their workflow and deliver visually stunning user experiences. For newcomers venturing into Vue.js, the initial setup might seem daunting. Thankfully, Vue.js offers a variety of component libraries to simplify this process. Quasar, PrimeVue, and Vuetify are standout options, each providing pre built tools to kickstart projects smoothly. Whether you prefer Quasar's extensive UI components, PrimeVue's Material Design inspired features, or Vuetify's responsive interfaces, these libraries cater to diverse preferences and project requirements. With their clear documentation and active communities, these libraries empower developers to start Vue.js projects confidently and efficiently, enabling Vue developers to create polished user experiences....

2 mins
VuetifyVueQuasar

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how <Suspense> and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly...

Jesús Padrón6 mins
VueWeb PerformanceUXJavaScript

Getting started with Vitepress

Create your static blog site using Vitepress. Setup and configure your site in minutes by following this step-by-step tutorial....

Simone Cuomo12 mins
VueViteVitepress