Łukasz Makuch

Łukasz Makuch

Scoped styles don't get applied to functional components in Vue 2

Having your CSS scoped to the component it applies to helps in maintaining encapsulation. That's exactly what Scoped CSS was designed for. It's pretty straightforward most of the time, but there are some controversial edge cases. One of them is the fact that these scoped styles defined by the parent component don't get applied to the root node of functional child components the same way they get applied to stateful child components. Here's an example of a parent component with scoped styles and a stateful child:

<template>
  <div class="layout">
    <Stateful class="child" />
  </div>
</template>
<script>
import Stateful from "./components/SomeStatefulComponent.vue";

export default {
  components: { Stateful },
};
</script>
<style scoped>
.layout {
  display: grid;
  grid-template:
    ". ...... ." 40px
    ". center ." 40px
    ". ...... ." 40px / 1fr 1fr 1fr;
  border: 1px solid black;
}

.child {
  grid-area: center;
}
</style>

It works as expected - the child is placed in the center of the layout: a stateful component gets styled

However, it breaks as soon as we make the child component a functional component:

<template>
  <div class="layout">
    <Functional class="child" />
  </div>
</template>

Now the child component doesn't get the styles from its parent: a functional component doesn't get the right styles

Why don't scoped styles apply to a functional child component?

If you've ever inspected the actual CSS of a component which uses scoped styles, you've probably noticed an attribute like data-v-80d8c89c. It's there to limit the scope of CSS selectors.

When styles are unscoped, there isn't much going on. We write templates like this:

<Stateful class="child" />

and styles like this:

.child {
  grid-area: center;
}

The generated DOM looks like this:

<div class="stateful">I am a stateful component!</div>

and the CSS stays exactly how we wrote it.

However, as soon as we make the styles scoped by changing <style> to <style scoped>, something happens. The generated HTML looks like this:

<div data-v-7ba5bd90="" class="stateful">I am a stateful component!</div>

and the styles have an extra attribute selector:

.child[data-v-7ba5bd90] {
    grid-area: center;
}

That's how the scope of the stylesheet is narrowed down so that it doesn't get applied to all .child nodes all over the app.

While this mechanism aims to scope all kinds of styles, what we're doing here is precisely styling child component root elements. It's this one special scenario in which scoped CSS is actually applied to some other components - the children. According to the documentation it should be used sparingly - just for layout purposes. Maybe that's why issues living in the junction of styling child component root elements and functional components don't arise too often - because as long as we keep our code idiomatic, it simply doesn't occur that often.

So, why does it break break when we make the child functional?

The CSS stays the same, that is scoped:

.child[data-v-7ba5bd90] {
  grid-area: center;
}

However, if the child has its own scoped stylesheet, it doesn't inherit the data-v attribute from its parent. In fact, it doesn't even accept the class the parent passes to it!

If the parent has

data-v-7ba5bd90

and tries to render

<Functional class="child" />

, we get:

<div data-v-064b6fca="" class="functional">...

Notice the different data-v and no "child".

How to style functional child components the same way stateful child components are styled?

The most straightforward way to address this issue would be to make this functional component behave the same way stateful components behave. We can bring back all the familiar bindings with 2 lines of code:

<template functional>
  <div
    class="functional"
    :class="[data.staticClass, data.class]"
    v-bind:[parent.$options._scopeId]="''"
  >
    I am a functional component!
  </div>
</template>

It works as expected - the child is centered: the functional component get the right styles

But is it a good scoped styles apply to child components?

This is a good moment to stop and think about the wise words of Dr. Ian Malcolm:

Dr. Ian Malcolm : Yeah, yeah, but your scientists were so preoccupied with whether or not they could that they didn't stop to think if they should.

If scoped styles are meant to be limited to the component they are defined in, is the fact that they are also applied to roots of children helpful or confusing? I believe it's the latter. Judging from my experience, most often than not this capability is used in a non-idiomatic way, for instance to style borders.

What are some alternative ways to style children components?

The most obvious way to style a child component is to use props. Props define a clear, obvious API.

And when we're actually defining a layout, pure CSS may be more than enough. For instance, a CSS grid container may define the align-items property which, according to MDN, sets the align-self value on all direct children. And if we need more control over how the direct children are styled, the child combinator > comes in handy:

<style scoped>
.layout /deep/ > * {
  align-self: flex-end;
}
</style>

Note the usage of a deep selector.

The resulting CSS is:

.layout[data-v-7ba5bd90] > * {

The deep selector disables scoping for this rule and that's why it's important that we narrow the selector down to the direct children of .layout ourselves throught he means of the child combinator >.

From the author of this blog

  • howlong.app - a timesheet built for freelancers, not against them!