Skip to content

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 be relevant but please refer to the relevant official documentation or available resources 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!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co