Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to make "scoped" components style leak optional? #957

Open
grigoryvp opened this issue Aug 27, 2017 · 56 comments
Open

Is it possible to make "scoped" components style leak optional? #957

grigoryvp opened this issue Aug 27, 2017 · 56 comments

Comments

@grigoryvp
Copy link

What problem does this feature solve?

Right now "scoped" is not really scope: styles from components leak to child components (this is by design, "A child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS."). Is it possible to make such feature optional? In practice, I often define simple class names like "root" for flex-placing children and it always leak to child components, wreaking hawok to ones that are unfortunate enough to use same short names.

Right now the workaround is to use "module" syntax, but it adds lots of unneded text, especially if compilers like PUG are used for CSS.

Making vuejs config option like "dontLeakScopedModules" with default to "false" will be a bless! I can create a patch, if it's an acceptable feature and i'm not getting anything of this wrong.

What does the proposed API look like?

{
test: /.vue$/,
loader: 'vue-loader',
options: {
dontLeakScopedModules: true

@LinusBorg
Copy link
Member

LinusBorg commented Sep 12, 2017

I'm not sure that I understand the problem. If you style a child component, it's root node will be affected (which you would want in this case , wouldn't you?) . If you don't, it doesn't. And I don't see how other nested elements within the child complement would be affected, unless the child was not scoped and used generic class names instead of a solid name space - which would be a problem of the child no matter if the parent uses scoped or not.

Yes, css modules offer a higher level of isolation in this case, but I don't see how to easily improve in this direction for "scoped" with our fundamentally changing the whole logic that we have in place.

But if you have a good idea, propose it, we are always happy to discuss new ideas.

@LinusBorg LinusBorg reopened this Sep 12, 2017
@grigoryvp
Copy link
Author

grigoryvp commented Sep 13, 2017

Thank you for your reply! Sorry for being not clear with description. If it's acceptable I can display a simple use case. A new developer starts a project by creating an empty dir and doing some simple command-line magic:

yarn add vue-cli
vue init webpack .
yarn install
yarn run dev

After playing around a bit they want to create a website with menu to the left and content to the right. Accoring to VueJS/ReactJS/AngularJS architecture this will be 3 components: one for left-right flexbox split layout, one for menu and one for content. Developer starts with layout, replacing Hello.vue with this:

<template>
  <div class="root">
    <div class="left">
      <LeftMenu></LeftMenu>
    </div>
    <div class="right">
      <div>Here be dragons...</div>
    </div>
  </div>
</template>

<script>
import LeftMenu from './LeftMenu.vue';
export default {
  components: {LeftMenu}
}
</script>

<style scoped>
.root {
  display: flex;
}
</style>

After that, developer creates a LeftMenu.vue component:

<template>
  <div class="root">
    <div>Here be left menu</div>
  </div>
</template>

<style scoped>
.root {
  margin: 10px 0 0 10px;
}
</style>

Developer expect "root" to be scoped into componen and do not affect other components. Even comment in sample app hints so: "". The "root" sounds like a short and reasonable name for root div which are completely different for container and for menu. Unfortunately, with beforementioned vue-loader feature the "root" class from container will leak into "menu" componen, making it flexbox which will be totally unexpected and hard to debug.

Introducing a configuration option can disable this style leak and make development much more straightforward. May I offer a patch / merge request / pull request so you can see that this is a really small change?

@johnathankent
Copy link

johnathankent commented Sep 16, 2017

Developer expect "root" to be scoped into componen and do not affect other components

You won't be able to prevent the CSS from cascading with a "non-leaking-style-scope" modifier.

Example:

<parent style="display: flex" non-leaking-style-scope>
  <child>No styles</child>
</parent>

Renders to:

<div class="parent" style="display: flex">
  <div class="child" style="display: flex">No styles.</div>
</div>

Why does this do that?
First, Vue components are not rendered in the shadow dom:

It is also totally feasible to offer deeper integration between Vue with Web Component specs such as Custom Elements and Shadow DOM style encapsulation - however at this moment we are still waiting for the specs to mature and be widely implemented in all mainstream browsers before making any serious commitments.
-- https://vuejs.org/v2/guide/comparison.html#Polymer

At the cost of a performance hit, the Shadow DOM could work. Full integration would mean a robust plugin (until it's more widely implemented) that could handle the back and forth of passing dynamic modules to the shadow-dom type component. You would get the benefit of styles that don't leak in or out. You might want to include options to pass in styles or stylesheets you want to inherit - lest you get a blank canvas. Here is one way you can add it yourself: https://github.com/karol-f/vue-custom-element

For those like me who would want to know why it doesn't work outside of the Shadow DOM:

CSS is designed to cascade styles from parents to children on the computed DOM tree. In CSS, there are elements and rules which inherit the values of the parent by default. (Flexbox is one of those.) Check out this guide by MDN. Since browsers ship with default styles, we don't need to write all the rule defaults for every single rendered element.

With Vue templates, all rendered html from components will end up in the DOM, and therefore be child elements of some parent element. The CSS code compiled from Vue template blocks <style>, <style scoped>, or <style module> still cascades on those.

So what is the point of Vue's <style scope|module>?

  • <style> rules cascade from global scope. Add .foo here and it affects other elements rendered on the same page following the rules of the CSS cascade.
  • <style scoped> is a basically a cool shortcut that behind the scenes just adds a more specific selector which restricts the cascade to only itself (and it's children).
  • <style module> dynamically simulates 'scoped'. It's children also follow CSS cascading rules, but restricted to itself and it's children. (i.e. If you change the parent to display: flex, the children will still inherit the style.)

Is it possible to make a "non-leaking-styles" method or turn off the cascading rules that display inherited [parent|browser-default] styles?

Your options:

  1. Turn off the Cascading in CSS, or just for that rule.
    • Turning off the cascade or inheritance rule would then require you to add the rule manually, for all the elements. And convince browser vendors it's a good idea to give you that option.
  2. Fake it behind the scenes by adding more CSS "reset-rules-to-what-the-state-would-have-been-rendered-as-previous-to-parent-changing-it". This would require a lot of additions to both JS and CSS. Not to mention your final CSS file would be huge, impacting performance, SEO, site-speed and UX. and the bugs. not the bugs. and the overrides of the overrides.
  3. Make a plugin that has the component become rendered in the Shadow DOM. Headstart with this: https://github.com/karol-f/vue-custom-element

Read up on the terms "specificity" and "inheritance" here:
https://www.w3.org/TR/CSS2/cascade.html#specificity
http://monc.se/kitchen/38/cascading-order-and-inheritance-in-css
https://www.w3.org/TR/CSS2/cascade.html#cascading-order
https://developer.mozilla.org/en-US/docs/Learn/CSS/Introduction_to_CSS/Cascade_and_inheritance
https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance

P.S. @grigoryvp For layouts, I highly recommend learning CSS Grid because it's children don't inherit the way flexbox does. Once it clicks, you won't want to go back.

EDIT: Forgot about the Shadow DOM

@johnathankent
Copy link

johnathankent commented Sep 16, 2017

For my coworkers who wonder why I am ok with waiting for the native handlers, the virtual-dom used by vue and react is faster:

Virtual DOM is any kind of representation of a real DOM. Virtual DOM is about avoiding unnecessary changes to the DOM, which are expensive performance-wise, because changes to the DOM usually cause re-rendering of the page. It allows to collect several changes to be applied at once, so not every single change causes a re-render, but instead re-rendering only happens once after a set of changes was applied to the DOM.
More precisely re-renders can and will quite heavily hit your hardware resources. Which will inevitably put your app performances into danger.
Shadow DOM is mostly about encapsulation of the implementation. A single custom element can implement more-or-less complex logic combined with more-or-less complex DOM. Shadow DOM refers to the ability of the browser to include a subtree of DOM elements into the rendering of a document, but not into the main document DOM tree.
-- https://vuejsfeed.com/blog/learn-the-differences-between-shadow-dom-and-virtual-dom

... and I happen to love what I can do with CSS. :)

@grigoryvp
Copy link
Author

Thank you.

Maybe this picture will help? DOM structure from my example above with problem highlighted and commented:

image

@johnathankent
Copy link

@grigoryvp
Current browser specs have no such thing as style "leaking" unless you create elements in a Shadow DOM.

While Vue core doesn't include adding Shadow DOM polyfills (it requires more code and reduced performance while browsers don't support it), you still have options:

  • If you consider yourself a beginner to intermediate javascript level, or if you want to save time and effort: Learn about how Cascading Style Sheets work. I mentioned some links above in my previous comments.

  • If you consider yourself advanced in javascript, don't want to learn about the C in CSS, or just have a scenario that requires sandboxing elements: You can add Shadow DOM elements to your app with Vue using a custom method or something like this plugin: https://github.com/karol-f/vue-custom-element. Be prepared for a lot of custom work and a smaller set of online support.

@grigoryvp
Copy link
Author

grigoryvp commented Sep 21, 2017

Thank you again.

As I explained before, maybe "leak" was a very bad word to describe the issue. As I illustrated in the screenshot, the problem is in the "data-v-b37e7c0a" HTML attribute that vue-loader adds in order for "root" class to be applied not only to the component where it's scoped, but also to the root element of a child component. This is a vue-loader feature described in the documentation:

"A child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS."

I propose to make an optional opt-out for this feature. I'm really sorry for that "leak" word and for spending your time.

@xanf
Copy link
Contributor

xanf commented Sep 21, 2017

Well, I think I understand the point which @grigoryvp states.
When we have a child component inside parent one - child component top-level node will receive both "data-" attributes - one from parent component instance and one from child. if both parent and child use same naming for css classes, like "root", that leads to pretty unexpected behaviour - when you're looking into template of parent component - you see no class name specified, so your expectation is that no styling from parent component will be applied. But since same class name is used inside child component template - it "magically" inherits styling of parent component.

In other words, when you're using scoped styles in that way, in order to understand styling of component it's not enough to look inside component template, since some classes with exactly same names may be pulled to root node from parent component. This makes things more complex when developing many components separately by big teams

@johnathankent
Copy link

Thank you @xanf and @grigoryvp for the clarification. I apologize for missing the obvious that you pointed out in the image.

I missed that Vue had added the parent scoping data attributes onto the child component root element (so that the parent can control layout). If I understand correctly:

What's expected:

Adding the "scope" attribute simplifies development of component styling by automatically reducing CSS specificity required, meaning I can use whatever CSS specifier I want, and style conflicts between components will not happen.

Problem:

Style conficts might happen regardless of scoping, and I really can't use whatever CSS specifier I want without conflict. The documentation is misleading by comparing the scoping to encapsulation done with the Shadow DOM like Polymer.

We have a current workaround:

Understand the caveats, update the documentation (removing Shadow DOM similarity; including with how Cascading inheritance applies to rendered DOM).

Current known caveats when using <style scope>:

  1. Styles cascade to child elements and components.
    • Parent style will cascade inheritance unless the child element overrides it by adding more specific selectors AND overriding the matching rules.
    • "deep" parent components also apply style inheritance to all matching selectors of nested elements or children components.
  2. Styles cascade from global to scoped components.
    • Global or dynamic styles will still cascade inheritance to scoped children.
  3. Matching selectors on children root elements may cause style confilcts.
    • Avoid using selectors that the parent elements use. (and who wins the conflict is determined by which selector is last in the rendered CSS.)
  4. Custom elements using the Shadow DOM can ignore cascading styles from the parent..
    • Custom elements are not part of Vue core, but a developer can add it to a Vue app.

@xanf
Copy link
Contributor

xanf commented Sep 21, 2017

Looks good to me!

Just a note for @grigoryvp and other's interested in that topic: while it is possible to make vue-loader not to add specific data attribute if at this point child component will be inserted, that will prevent some typical and useful scenarios, when you have some styling applied via css classes in parent component, and some - in child on the same node. It's definitely a frequently used case

@johnathankent
Copy link

Before we recommend a patch or solution to be implemented, I think a discussion should be had regarding methods to remove the data-scope from the child root element. I predict problems if we recommend and/or implement something that doesn't follow a pattern. Already, the additional option will continue to have the same 'this is not obvious functionality' problem when trying to determine why scoping was/wasn't followed.

Points to discuss:

  • What would be most transparent for developers?
  • What would be easiest to develop, manage, and follows a pattern?
  • Where is it best to apply the option/flag?
    • In the parent template as an option? Vue options/misc: { dontScopeLittleBros: true }
    • In the child template as an option? Vue options/misc: { dontScopeMeBro: true }
    • As an attribute on the child element in the parent? <child-component dontScopeMeBro>
    • As another attribute on style in the parent? <style scoped dontScopeLittleBros>
    • As an attribute in the child? <template dontScopeMeBro>?
  • How will true encapsulation via Shadow DOM fit the pattern when it is finally supported by browsers? <template encapsulated>, or Vue options/misc: { encapsulated: true }

@grigoryvp
Copy link
Author

grigoryvp commented Sep 24, 2017

Just had a heated deabed with our front end team lead. It seems that simplest way to "opt-out" from this feature is to add an empty root div, which is very easy. And feature itself seems to be usefull since it allows to modify non-cascading styles for root element like "margin", "border-radius" etc. So I humbly propose following improvement into docks, we can replace

"With scoped, the parent component's styles will not leak into child components. However, a child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS. This is by design so that the the parent can style the child root element for layout purposes."

with something like:

"With scoped, the parent component's styles will not leak into child components. However, a child component's root node can use classes from both parent's scoped CSS and it's own scoped CSS. This feature allows parent component to change child component's root node non-cascading styles like "margin" or "border-radius". To disable parent component's class names visibility for child component's root node just add a "div" root node without classes to the child component"

If it's acceptable I will do an English proof read and submit a patch

@johnathankent
Copy link

@grigoryvp That is a simple workaround, another is to avoid using the same classnames on the root node.

I also think this topic should be revisited when Shadow DOM support is going to be added to Vue core if it makes sense.

@johnathankent
Copy link

johnathankent commented Sep 25, 2017

@grigoryvp @xanf @LinusBorg I think the docs needed a bit more to help fix the confusion, so I also took a crack at updating the documentation for Scoped CSS. Let me know what you think (forked version here): https://github.com/johnathankent/vue-loader/blob/53a9f3ab52d63aabe9304a61b89f99392fac620c/docs/en/features/scoped-css.md

@grigoryvp
Copy link
Author

Thanks, @johnathankent

But I can't see any info that this feature can be disabled by adding empty parent 'div' element. Did I miss something or such info is not important?

Also, offtopic, you wrote that another workaround is to avoid using the same classname on different components. But how is it possible for medium to big sized projects with hundreds or thousands of components? Human focus can handle only 7 plus-minus 2 items at a time :( Or if different developers are creating components - how can they check that root element's classes are unique?

@johnathankent
Copy link

johnathankent commented Sep 29, 2017

@grigoryvp I feel your pain. Communicating front-end CSS and how it works is hard.

The documentation as it currently stands kinda says 'leaks' and I think that has caused some added confusion, so that's why we need to help fix it.

Cascading inheritance is not something that can be stopped. Styles cascade (not 'leak'), and we cannot disable cascading as a 'feature'. Cascading styles are browser functionality and not Vue.js features. Without Shadow DOM custom elements, disabling and leak fixes just ain't gonna happen. Vue components help "encapsulate" styles, but all that means is it's adding specificity so style cascades stay local to itself and all it's rendered children.

But I can't see any info that this feature can be disabled by adding empty parent 'div' element. Did I miss something or such info is not important?

I thought that's what you meant in your earlier comment:

Just had a heated deabed with our front end team lead. It seems that simplest way to "opt-out" from this feature is to add an empty root div, which is very easy. And feature itself seems to be usefull since it allows to modify non-cascading styles for root element like "margin", "border-radius" etc.

I use a pattern where external parent components or stylesheets handle layout using the root node in Vue Components, so I avoid setting styles on it. I think that's what the documentation meant to convey in the first place. And even with wrapper divs, cascading styles are not going to be removed or prevented.

Used as a simple pattern might lessen conflicts, but it's still no guarantee that cascading inheritance will not conflict with styles from a Vue child component.

Also, offtopic, you wrote that another workaround is to avoid using the same classname on different components.

Yeah, I wasn't clear there. I was thinking of team project styleguides. In large teams or project cases as you point out, documenting or syncing classnames is not simple. In that case, a pattern is perfect use case.

I added a couple pattern and tried to clarify a bit more. Let me know what you think: https://github.com/johnathankent/vue-loader/blob/74603af204cc449af2857e1e63e12246cc48ee95/docs/en/features/scoped-css.md

Edit: I added a couple changes and realized that all the "Keep in mind" section was technically styleguides. So I propose that we could move the styleguide to a separate page and reference it early - in order to keep it clean and simple.

Updated link here: https://github.com/johnathankent/vue-loader/blob/111bc28c3eaa478ec1b0cdf5943dd7d2545b8ea9/docs/en/features/scoped-css.md

@johnathankent
Copy link

More updates!

I made clarified the headers a bit, and created a troubleshooting section to the styleguide. See: https://github.com/johnathankent/vue-loader/blob/f0c830e6839ab2e9ce04891421ffd1b846c3fde8/docs/en/features/scoped-css.md

@grigoryvp
Copy link
Author

Lots of text, but it explains the point about child root nodes and how they are styled by parents. Thanks a lot for the hard work and sorry for all the troubles!

@johnathankent
Copy link

@grigoryvp Ain't no trouble. Happy to help. I also apologize for not understanding the real problem at first, too.

Anything else we could do to make the doc easier or cleaner before we submit it?

@trusktr
Copy link

trusktr commented Jan 5, 2018

We've almost got a solution!

As soon as vuejs/vue#7385 is fixed (if it isn't already and I missed it) then we can easily use SkateJS to make Custom Elements written as Vue single-file-components that can be composed by nesting them, and have shadow roots to encapsulate inner parts.

This would keep styles from cascading to child components!

At the moment, vue-custom-element does not support nesting of the generated custom elements, while using SkateJS with a Vue renderer does.

Here's how you generate custom elements from Vue components using SkateJS (it works, except for the above mentioned styling issue):

skatejs/skatejs#1289 (comment)

Note the RealSlot definition and the use of <real-slot> in App's template so that we can place a real real slot element there. By default SkateJS automatically creates an element's shadow root, and we used <real-slot> to specify where to distribute the element's content.

The content in the shadow root is generated by the Vue component! So you can use that VueElement function to turn any Vue component into a Custom Element. 😄

Output looks like the following, and notice the inspector shows both elements nested:

screenshot 2018-01-04 at 6 43 14 pm

So, once we figure out how to get styles placed inside of shadow roots, then we'll be good to go!!

😉

@trusktr
Copy link

trusktr commented Jan 5, 2018

Okay, admittedly, this doesn't wire up the attributes to the component props, yet...

@trusktr
Copy link

trusktr commented Jan 22, 2018

Alright you all, I published my solution. Styles work and won't leak into sub-roots: https://github.com/trusktr/vue-web-component

There's some listed caveats, but planning to improve this.

@robyedlin
Copy link

+1 for being able to turn this feature off.

https://github.com/johnathankent/vue-loader/blob/f0c830e6839ab2e9ce04891421ffd1b846c3fde8/docs/en/features/scoped-css.md#layout-and-child-components

@alianrock
Copy link

+1 for being able to turn this feature off

@grigoryvp
Copy link
Author

Looks like the same problem then :(

@oles
Copy link

oles commented Jul 5, 2018

Tried to find out where exactly this happens, hoping it would be a simple enough task to send in a PR for it... and it seems like this happens in Vue's core https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js#L337

Already feel like I'm way over my head. Am I right in that a PR for a options: {leakyScopedCSS: false} for https://github.com/vuejs/vue-loader would require another PR for https://github.com/vuejs/vue?

@morningdew830
Copy link

morningdew830 commented Aug 16, 2018

Hey, I really need to use something like { leakyScopedCSS: false}
Hasn't it merged yet?
I use the karma-snapshot for unit testing. here is the karma config snippet.
The problem is that snapshots are differently generated per OS platform when I run the unit test.
For instance,

  • When run the unit test on Windows x64:
<v-container
  data-v-3546496c=""
>
  • when test on macOs or linux
<v-container
  data-v-98fc5b54=""
>

Is there an option to make a OS platform independent data-v-id in vue-loader?
Looking forward that { leakyScopedCSS: false} option is activated.
Thanks for your help in advance!

For reference, here is my karma config snippet.

module.exports = function karmaConfig (config) {
  config.set({
    browsers: ['ChromeHeadless_without_sandbox'],
    frameworks: ['mocha', 'snapshot', 'mocha-snapshot'],
    reporters: ['mocha', 'coverage'],
    files: [
      '**/__snapshots__/**/*.md',
      '../../node_modules/babel-polyfill/dist/polyfill.js',
      './index.js'
    ],
    preprocessors: {
      '**/__snapshots__/**/*.md': ['snapshot'],
      './index.js': ['webpack', 'sourcemap']
    },
    webpack: webpackConfig,
    ... ... ...

@morningdew830
Copy link

the latest version fixed my issue above. I am aware of what fixed it.
lib/index.js

const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery

Thanks vue.js team!

@phodge
Copy link

phodge commented Aug 31, 2018

@johnathankent: I'm investigating using Vue for a large project at my workplace and was extremely discouraged to uncover this bug by accident after following the simple examples on https://vuejs.org/v2/guide/single-file-components.html

It would be great if you could get your updated docs merged, but probably more importantly, the examples on the introductory page linked above desperately need to be changed because they encourage writing SFCs in a way that makes unexpected CSS leakage extremely likely.

@vuejs vuejs deleted a comment from gregorybolkenstijn Aug 23, 2019
@IlyaSemenov
Copy link

IlyaSemenov commented Sep 3, 2019

I've built a simple Vue directive that explicitly stops propagation of data-v-XXXX attributes: https://github.com/IlyaSemenov/vue-remove-scope-directive

Example usage:

<template>
	<div>
		<!-- This h1 is styled -->
		<h1>Welcome</h1>
		<main v-remove-scope>
			<!-- This h1 is not styled -->
			<h1>Willkommen!</h1>
			<!-- Neither are slot contents -->
			<slot />
		</main>
	</div>
</template>

<style scoped>
	h1 { color: red }
</style>

It recursively updates el.dataset and removes $options._scopeId. This seems to work even for partial updates and hot reload, but YMMV.


UPDATE this doesn't seem to work well with multiple nested slots. I switched to using simple <wrap> component to stop style leak:

Vue.component("wrap", {
	render(h) {
		return h("div", this.$slots.default)
	},
})

@tschoartschi
Copy link

Any update on this? Today I run into the same issue. I had a class called container in the parent and child. Therefore the parent overrode the definition in the child. This was very subtle because it only changed the view by some pixel so I'm thinking 🤔 what is a good way to prevent these kinds of clashes.

I hoped that scoped css removes the need for verbose names like BEM etc.

@Pithikos
Copy link

Pithikos commented Jun 1, 2020

Yes, this is pretty annoying. And although in some cases you can use non-colliding naming as suggested, it is a too easy to run into the same issue multiple times.

<div>

  <!-- Parent button -->
  <button type="button" class="btn btn-primary">
    Next
  </button>

  <Toolbox>
    <div slot="buttons">
      <!-- Child button -->
      <button type="button" class="btn btn-danger">
        Delete
      </button>
    </div>
  </Toolbox>

</div>

In this case, styling .btn in the parent component screws up the Toolbox buttons. And since all this uses bootstrap, it's not possible to simply change class names. Only solution is to keep adding classes which makes the code pretty ugly.

@shahzore-qureshi
Copy link

Talk about a deal breaker. This methodology of uncontrolled cascading directly conflicts with the idea of modularization. When a project has more than fifty components with various levels of nesting, how does one expect to keep track of unique CSS class names per component? I may as well go back to 2002 and just create one gigantic CSS file where I can better manage and track unique class names.

The point of modularization is reuse. Unfortunately, with this uncontrolled cascading, importing a component somewhere will most likely lead to unexpected conflicts. It is clear that the creators of this framework and maintainers like @johnathankent are holding on to the ideas of the 2000s (i.e. long and unmaintainable cascading sheets) while also trying to incorporate ideas of today. I am also disappointed that this thread has led to no real solutions and surprised that it is still open.

I think it's time to go back to Angular and React, where controlled cascading and modularization actually work.

@wokalek-work
Copy link

+1

Piece of garbage, pointless feature

@sthales
Copy link

sthales commented Sep 2, 2020

+1 for being able to turn this feature off

@wokalek-work
Copy link

But temporary I found this solution:

Test.vue:

<template>
    <div>
        <div class="test">
            <TestNested/>
        </div>
    </div>
</template>

<style lang="sass" scoped>
.test
    width: 200px
    height: 200px
    background: red
</style>

Nested.vue:

<template>
    <div>
        <div class="test">

        </div>
    </div>
</template>

<style lang="sass" scoped>
.test
    width: 100px
    height: 100px
    background: orange
</style>

So, you wrap every component with empty div that gets v-data-{id} attribute.

This way the inner class .test doesn't try to override the attributes of the parent component, because it doesn't inherit the attribute at all.

@IlyaSemenov
Copy link

@vostokwork This approach works fine until you want to maintain the chain of nested display: flex elements.

For instance, consider layout.vue:

<script>
export default {
  computed: {
    component () { return x ? Page1 : Page2  }
  }
}
</script>

<template>
<div>
  <div class="layout">
    <component :is="component" />
  </div>
</div>
</template>

<style scoped>
.layout { display: flex; flex-direction: column; }
</style>

and page.vue:

<template>
<div>
  <div class="page1">Page 1</div>
</div>
</template>

<style scoped>
.page1 { display: flex; flex-direction: column; } // not working as expected
</style>

The flex chain breaks, which affects e.g. nested scrolling in vertically constrained elements.

My current approach is to dedicate a special class prefix for top-level component elements, such as class="c-page1" and never use c-xxx for inner elements.


Also, both these approaches suffer from an another problem: style leak when a parent styles the nested component.

Consider parent.vue:

<template>
	<div>
		<div class="parent">
			<Child class="child" />
		</div>
	</div>
</template>

<style scoped>
.child {
	border: 1px solid red;
}
</style>

and child.vue:

<template>
	<div>
		<header>Child Header</header>
		<div class="child">Child Content</div>
	</div>
</template>

<style scoped>
.child {
	color: blue;
}
</style>

Result:

Screenshot 2020-09-09 at 15 42 50

The child header becomes blue, even though it's not referenced by either <style> block.

@1beb
Copy link

1beb commented Jan 30, 2021

This got me recently. Good to see I wasn't the only one. I don't remember seeing this in the vue docs. Is there a section that discusses this in there that I glazed over?

@1beb
Copy link

1beb commented Jan 30, 2021

After a bit of digging I did find it in loader docs. https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles. I wonder if there should be a shoutout to it somewhere in the essentials.

@realityfilter
Copy link

+1 for being able to turn this feature off

Maybe with the following simple syntax <script scoped="mode"> would be sufficient.
With modes for the current behavior and a restricted one for only html tags in the template.

@molvqingtai
Copy link

+1 for being able to turn this feature off

@JustASquid
Copy link

JustASquid commented May 9, 2022

I can't believe this is still open! As an experienced React developer, I'd been having a great time learning Vue until I ran into this glaring flaw. Such an obvious and fundamental issue sticking around for almost 5 years leaves me concerned... what other fundamental problems could be lurking around?

Has anyone found a workaround/naming convention that:

  1. Guarantees class isolation between components
  2. Doesn't require runtime-altering changes, like adding a div wrapper around every component

@vincerubinetti
Copy link

vincerubinetti commented Jul 21, 2022

Here is yet another demonstration of this problem, based on how I ran into it.

https://codesandbox.io/s/vue-scoped-style-problem-ibpskr

Icon.vue

<template>
  <div class="icon">icon</div>
</template>

<style scoped>
.icon {
  color: blue;
}
</style>

App.vue

<template>
  <Icon class="icon" />
  <Icon />
</template>

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

Top text should be red, bottom should be blue. Instead they are both red. Critically: If you change the .icon class name in either file so that they don't conflict with each other, the problem goes away.

I feel that this is a very insidious problem that can easily go unnoticed for a long time until it causes a very hard to track down bug. Most importantly, I don't see this edge case documented anywhere.


Other than having an option to turn this "leaking" completely off for a whole component or the whole app (which I admittedly wouldn't want either), I can't think of any good solution to this that wouldn't make things more complex and unexpectedly break things. It's just an inherent flaw with how it works. As Linus said:

but I don't see how to easily improve in this direction for "scoped" with our fundamentally changing the whole logic that we have in place

As such, css modules might be more fool proof. This seems to be closer to how other frameworks like React tend to do scoped css, with hashed class names like .SomeComponent_1a2b3c. Everything is just based on class names only, and you explicitly attach them to the elements you want (unlike data-v-XXX being added to everything).

Like others have said, wrapping every component in a <div> is unacceptable.

@IlyaSemenov
Copy link

IlyaSemenov commented Jul 21, 2022

Unless I'm mistaken, with css modules there's no way you can style child component's inner elements from within the parent component. The $style variable is only available within the respective SFC, it can't be imported from outside.

@vincerubinetti
Copy link

vincerubinetti commented Jul 21, 2022

Yes you're right. If you're trying to select an element in a child component that's deeper than the root element, and you're trying to do it with classes, that's not possible with css modules.

You could still do it with generic selectors, like .this-component > div { /* apply styles to child component made of div */ }, but that gets ugly quick.

Luckily it's very rare in my code base that I need the :deep() selector, so I'm still probably going to switch to CSS modules.

Another possibility might be using module in most cases, and adding scoped where deep selectors are needed (hopefully rarely). Testing it out, it seems like they can co-exist in the same component:

<template>
  <div :class="'gallery ' + $style.gallery">
    <slot />
  </div>
</template>

<style lang="scss" module>
.gallery {
  display: grid;
}
</style>

<style lang="scss" scoped>
.gallery > .some-child {
  margin: 0 !important;
}
</style>

It looks like CSS modules have been considering a way to import hashed class names for reasons like this, but it doesn't seem like vue-loader supports this:

css-modules/css-modules#147

@jacekwilczynski
Copy link

jacekwilczynski commented Oct 26, 2022

+1 for being able to turn this "feature" off... where by "feature" I mean a complete encapsulation break that defies the very point of having scoped styles. If you really want to style children, you have the deep selector for that. But generally you shouldn't. If you want to create e.g. a flexbox-based layout, where you need to style both the container and the items, then you shouldn't mix the responsibilities of layout item & whatever the child is anyway. So you're better off doing something like this (but not possible now due to the leak):

<!-- parent: Layout -->
<template>
<div class="root">
  <div class="item">
    <Card />
  </div>
  <div class="item">
    <Card />
  </div>
</div>
</template>

<!-- child: Card -->
<template>
<div class="root">
   ...
</div>
</template>

This is a structure that actually helps separation of concens & style decoupling, allowing you e.g. to use padding for gutters without preventing the children from having inner paddings of their own. In some cases this separation may be redundant, but you only do it when you need it. Contrast that with the earlier proposed hack where you wrap every component in a dummy div, which adds no runtime value and just bloats your element tree (imagine what your whole page will look like!).

<!-- parent: Layout -->
<template>
<div>
  <div class="root">
    <Card />
    <Card />
  </div>
</div>
</template>

<!-- child: Card -->
<template>
<div>
  <div class="root">
    ...
  </div>
</div>
</template>

@Hai-San
Copy link

Hai-San commented Jun 6, 2023

This is clearly an issue with the current way we work, separating everything into components, it is obvious that the style has to be isolated only in the component and not leak to children, unless I myself decide that this should happen. CSS modules do this perfectly and have not received an update for years. The only problem with css modules is bad writing.

@altrusl
Copy link

altrusl commented Jul 11, 2024

Turning this feature off means a lot of "dirty" work and will add unneeded complexity to the framework
Instead you can use CSS nesting (which is a good practice) and this will solve all related possible problems

@IlyaSemenov
Copy link

@altrusl In the example by @jacekwilczynski just 3 messages above, if the parent component has a rule: .root { padding: 100px } it will leak to the child component. How do you propose use CSS nesting to solve "all related possible problems"? The parent root div is not even nested. Can you come up with specific code that will solve the problem for the case above?

@ikanadev
Copy link

ikanadev commented Aug 10, 2024

First of all, I'm not complaining. There is no reason to get upset about this "issue" after everything VueJS has given us.

I just want to share my not so great experience I had with this feature.

If you use radix-vue and create a custom <Tooltip /> just like the docs suggest and use it in a component with scoped styles you will see this in your terminal.

2024-08-10_01-08

After spending some (a lot) of time trying to figure out what is happening, my best guess is that Vue tries to pass the data-v-XXXXXXXX to the <TooltipRoot> and hence the warning.

My current workaround is add a ghost <span /> along the <TooltipRoot>, this will prevent Vue to try to add the data-v-XXXXXXXX on it, like this:

<template>
    <TooltipRoot>
        <TooltipContent>...<TooltipContent/>
    </TooltipRoot>
    <span class="sad-span-without-reason-to-exist" />
<template/>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests