vue-loader icon indicating copy to clipboard operation
vue-loader copied to clipboard

:root selector for scoped CSS

Open davidhewitt opened this issue 6 years ago • 21 comments

What problem does this feature solve?

Often when I'm writing scoped CSS for my components I need to put a class on the root element so that I can style it. For example:

<template>
    <div class="foo-root">Hello World</div>
</template>

<style scoped>
    .foo-root {
        color: red
    }
</style>

What does the proposed API look like?

CSS already supports a :root selector which means the "root HTML element of the document" (i.e. usually the <html> element).

As far as I know it's never possible for a component to be the root <html> element of the document. So this :root selector effectively does nothing inside scoped CSS.

Therefore, why not repurpose it to mean the component's root element? We could then re-write the example above without needing to add a specific class:

<template>
    <div>Hello, World</div>
<template>

<style scoped>
    :root {
        color: red;
    }
</style>

Admittedly a small idea, but one that I think would save a fair bit of typing (and inventing class names)!

davidhewitt avatar Oct 22 '19 14:10 davidhewitt

+1024 for this one!

bpolaszek avatar Dec 19 '19 08:12 bpolaszek

I had this in mind as well, good idea.

adi-works avatar Mar 15 '20 02:03 adi-works

Guys :root really means <html> and that meaning is very much cemented. Vue shouldn't mess around with that.

:host is closer to what you're looking for and it's what Riot.js uses for this same use case, so support for that in Vue would be much appreciated!

jfbrennan avatar Apr 13 '20 18:04 jfbrennan

...come to think of it, the solution can be as simple as:

<template>
  <div>
    // other markup
  </div>
</template>

<style scoped>
  div:first-child { ... }
</div>

Maybe this is what a lot of people already do?

jfbrennan avatar Apr 13 '20 18:04 jfbrennan

:host seems like a better alternative to the :root I originally proposed!

davidhewitt avatar Apr 13 '20 19:04 davidhewitt

Or to think of it... how about $el?

If that's valid CSS syntax - would be really natural to use given $el is already how we access the component's element inside <script>...

davidhewitt avatar Apr 13 '20 19:04 davidhewitt

...come to think of it, the solution can be as simple as:

<template>
  <div>
    // other markup
  </div>
</template>

<style scoped>
  div:first-child { ... }
</div>

Maybe this is what a lot of people already do?

I just tried it, and it doesn't work. Besides, this assumes you have a knowledge of the first element being a div, meaning this could not applied to this:

<template>
    <a v-if="isLink" href="...">{{ text }}</a>
    <span v-else>{{ text }}</span>
</template>

:host or :root appear like more generic solutions.

bpolaszek avatar Apr 19 '20 16:04 bpolaszek

@davidhewitt In my opinion:

:host looks semantically like a new concept, it sounds like "we couldn't use :root so we had to find another term".

$el doesn't look CSS at all.

As you pointed out, :root means "root HTML element of the document" (i.e. usually the element) - so it totally makes sense in a scoped context.

@jfbrennan :root doesn't necessarily mean <html>, according to MDN:

The :root CSS pseudo-class matches the root element of a tree representing the document. In HTML, :root represents the element and is identical to the selector html, except that its specificity is higher.

Perfectly fine in our case IMHO.

bpolaszek avatar Apr 20 '20 05:04 bpolaszek

it sounds like "we couldn't use :root so we had to find another term".

:host is a normal CSS pseudo-class though, and it is absolutely appropriate here since Vue’s component syntax is loosely modeled after the [Web Component] spec.

$el doesn't look CSS at all.

True.

@jfbrennan :root doesn't necessarily mean <html>, according to MDN:

The :root CSS pseudo-class matches the root element of a tree representing the document. In HTML, :root represents the element and is identical to the selector html, except that its specificity is higher.

Perfectly fine in our case IMHO.

What about this sounds "Perfectly fine"? 😅 The definition you yourself have quoted states:
"In HTML, :root represents the element and is identical to the selector html"

Yes, :root doesn't necessarily mean html because it can be used in SVG documents, too – but this is quite clear-cut, usage in HTML → html selector → impractical for the proposed usage as Vue component root.

jonaskuske avatar Apr 22 '20 01:04 jonaskuske

FWIW, if anyone's feeling passionate about pushing this forward and has a spare evening, it would probably be a great time to write a Vue 3 RFC on this feature. Especially inspiration could be drawn from https://github.com/vuejs/rfcs/pull/119

(I'm too busy on other projects at the moment or I might have considered writing the RFC myself)

davidhewitt avatar Apr 22 '20 07:04 davidhewitt

:host is a normal CSS pseudo-class though, and it is absolutely appropriate here since Vue’s component syntax is loosely modeled after the [Web Component] spec.

My bad, I should have googled more 😅I definitely thought it was an invention of some framework to do the trick we wanted to copy/paste. I go back on my word then, :host sounds better indeed 🙂

bpolaszek avatar Apr 22 '20 08:04 bpolaszek

Is this issue stalled?

I would like to upvote the:host suggestion. Why? because it is necessary to select the root element of a component without using an id or class attribute. I think it is important for re-useable components.

Example structure

<template>
    <div>
        <div>Hello World</div>
    </div>
</template>

Setting a style for the outer div cannot be accomplished with div:frist-child (as suggested in this thread) because it would also match the inner div.

Using a CSS class with scoped context leads to a HTML document which is not nice when the component is used

<template>
    <div class="foo">
        <div>Hello World</div>
    </div>
</template>
<style scoped>
.foo {
    color: red
}
</style>

When someone now uses my component

<Foo></Foo>

and adds a CSS class called "foo"

<Foo class="foo"></Foo>

the generated HTML looks like

<div class="foo foo" data-v-654321 data-v-123456>
    <div>Hello World</div>
</div>

I just want to use styles inside my component for my root element without weird CSS usage.

So I would love to use

<template>
    <div>
        <div>Hello World</div>
    </div>
</template>
<style scoped>
:host {
    color: red
}
</style>

Which then should lead to

<div class="foo" data-v-654321 data-v-123456>
    <div>Hello World</div>
</div>

when used as described above.

giftkugel avatar Jun 24 '20 14:06 giftkugel

Was RFC created for this?

rightaway avatar May 16 '21 07:05 rightaway

Curious about this too! Maybe someone in the know could update this with the current best practice (if any)? If no feature has been added to provide even an oblique solution then perhaps there should be an RFC.

MattiasMartens avatar Feb 01 '22 18:02 MattiasMartens

Something that just complicates everything is that from Vue 3 on, a <template> can legitimately host multiple children 😃

bpolaszek avatar Feb 01 '22 19:02 bpolaszek

<template> can legitimately host multiple children :smiley:

Nonetheless :host > :first-child or :host > nav should be a valid selector.

patarapolw avatar Apr 20 '22 10:04 patarapolw

Something that just complicates everything is that from Vue 3 on, a <template> can legitimately host multiple children

Not seeing how that changes or complicates this.

pecknigel avatar Jun 19 '23 15:06 pecknigel

<template> can legitimately host multiple children 😃

Nonetheless :host > :first-child or :host > nav should be a valid selector.

I think this would add confusion if we need to use :host for a single-root component and :host > :first-child for a multi-root component.

In my opinion, :host should match any "root" element of the component.

In this example, both div should have a border.

<template>
  <div>I have a border</div>
  <div>I have a border</div>
</template>

<style scoped>
:root {
  border: 1px solid blue;
}
</style>

If you want to style only the first one:

<template>
  <div>I have a border</div>
  <div>I DON'T have a border</div>
</template>

<style scoped>
:root:first-child {
  border: 1px solid blue;
}
</style>

ByScripts avatar Mar 07 '24 07:03 ByScripts