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

[css-scoping] Please bring back scoped styles #3547

Open
o-t-w opened this Issue Jan 22, 2019 · 29 comments

Comments

@o-t-w
Copy link

o-t-w commented Jan 22, 2019

The scoped style tag HTML attribute works really well with popular frameworks and is far simpler to use than shadow DOM.

e.g. in React:

import React from 'react';

const Profilecard = (props) => {
	
	const styles = `
	div {background-color: ${props.bgColor}}
	h2 { color: white; }
	img  { 
		border-radius: 50%;
		height: 80px;
		width: 80px;
	}`

	return (
		<div className="card">
			<style scoped>{styles}</style>
				<h2>{props.name}</h2>
				<img src="man3.jpg" alt=""/>
			</div>
	);
};

export default Profilecard;

Shadow DOM offer more encapsulation than scoped styles, but this level of encapsulation is not often needed, and even sometimes complained about by developers.

e.g. Chris Coyier:

Personally, I wish it was possible to make the shadow DOM one-way permeable: styles can leak in, but styles defined inside can't leak out.

It is important to note that both Vue js and Svelte have replicated this API - it is clearly an API that is both easy to work with and gives the right level of encapsulation (unlike shadow DOM, which is useful for more niche cases).

At the moment people are using many different solutions (CSS Modules, Styled Components and all the other CSS-in-JS options). It would be great if there was a standardized way to solve the problem of scoping rather than the very fragmented options in user-land. I strongly doubt that shadow DOM will prove to be a popular solution capable of attracting people away from these libraries.

A prior discussion about the topic can be found here: #137

@tomhodgins

This comment has been minimized.

Copy link

tomhodgins commented Jan 22, 2019

I would love something like this. I've been experimenting with a custom HTML tag I call <style-container> that acts as a container for scoping CSS styles, and I'm also exploring the idea of interpreting contained media queries as container queries based off the <style-container> tag's dimensions, here's how I'm working with it:

<style-container>

  <div>Make me 500+ px wide</div>

  <style media=none>
    div {
      border: 1px solid;
      padding: 1em;
    }
    @media (min-width: 500px) {
      div {
        background: lime;
      }
    }
  </style>

</style-container>

<script type=module>
  import styleContainer from 'https://unpkg.com/style-container'

  styleContainer()
</script>

From what I can see there are two main benefits:

  1. Having something like this would be good for modularizing code - you could safely include stylesheets in certain HTML elements and be sure they weren't going to apply to other elements.

  2. Another potential benefit — is it possible that interpreting media queries as based on the containing element's dimensions could be a way that existing CSS written with media queries can be re-used for modular responsive styles without having to refactor existing styles or introducing new syntax?

@BrentARitchie

This comment has been minimized.

Copy link

BrentARitchie commented Jan 27, 2019

@o-t-w I also, would like to see the re-visiting of scoped styles and would love to see this make a return. Being able to define a style that is scoped to a subset of elements would go a long way in terms of major use cases such as branding, consistency and the application of design systems in general.

@tomhodgins You bring up a good point, what should this spec exactly cover? Simply the scoped keyword? As you mentioned there are lots of ideas that could bleed into this such as media queries, @if and @else. Would these other points belong in an elements or container query spec? or would you want to have them all added under the scoped styles spec?

If memory serves me, it was the vendors that pushed back on the spec initially. Maybe we need to revisit their concerns and see what can be done to mitigate them as a start?

I would be in favor of focusing on the scoped html attribute and possibly the reinstatement of the @Scoped keyword. Limiting the spec, I think, would help make it a lot more palatable for browser vendors.

@AmeliaBR

This comment has been minimized.

Copy link

AmeliaBR commented Jan 28, 2019

I think we need to separate the discussion of scoped styles as a concept from the specifics of the scoped attribute on a <style> element. There are other syntaxes that could be used (an @-rule, maybe?) that could serve the same goal with different side effects and fallback behavior.

If memory serves me, it was the vendors that pushed back on the spec initially. Maybe we need to revisit their concerns and see what can be done to mitigate them as a start?

I fully support this strategy.

Can anyone outline specifically why decisions were made to not implement the scoped attribute? For me, the major issue was progressive enhancement. I couldn't accept the fallback behavior to be the scoped styles applying to the entire document—although this is really only an issue when the scoped styles are in the markup, since you can always test for attribute support in JS.

But maybe there were other reasons from a browser performance perspective?

I'd like to think that by rolling this back to the underlying needs and concerns, we can come up with a different syntax that addresses the needs without the same concerns.

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 28, 2019

The reason why the scoped attribute idea was abandoned is that, in contrast to ShadowDOM, it didn't come with any lower boundary. This means that styles affect an entire subtree rather than a single "component". In fact this is not different from prefixing every selector with a unique id eg. .foo { } becomes #scope-unique-id .foo {}.

Svelte, Vue, Angular but also styled-jsx (React) instead scope the styles to a single component to simulate ShadowDOM. eg:

<style>
div { color: red }
div h1.foo { color: green }
</style>

<div>
   <h1 class="foo">howdy</h1>
</div>

in those frameworks becomes:

<style>
div.scoped-123.scoped-123 { color: red }
div.scoped-123 h1.foo.scoped-123 { color: green }
</style>

<div class="scoped-123">
   <h1 class="foo scoped-123">howdy</h1>
</div>

This is done at build time.

@prlbr

This comment has been minimized.

Copy link

prlbr commented Jan 28, 2019

The already existing effort to implement it in Blink was actually removed because developers found it to be too complicated to develop scoped and ShadowDom in parallel. Developers said this would not be a final nail in the coffin of scoped and that it could be implemented later (e.g. internally based on ShadowDom?). See https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/JB8nFQXhAuQ

Edge didn’t implement it for whatever reason, but this does not seem relevant anymore since Microsoft ditched its effort to develop an own browser engine altogether.

Firefox on the other hand did implement scoped. They removed it mainly because it was removed from the specs after other browser engine vendors did not implement it. https://groups.google.com/forum/#!topic/mozilla.dev.platform/iBoROFkR9V8

@SelenIT

This comment has been minimized.

Copy link
Collaborator

SelenIT commented Jan 28, 2019

Interestingly, per the current CSS Cascading Level 4 spec,

Normal declarations from style attributes are considered to be scoped to the element with the attribute

Doesn't this imply that, once CSS Nesting makes it into standard, it would be possible to write element-scoped styles as nested selectors inside the style attribute?

@Heydon

This comment has been minimized.

Copy link

Heydon commented Jan 28, 2019

+1 to scoped styling making a return.

Here's how I see it:

  1. Most of the time I want access to global / parent styles for my components.
  2. If I don't want that, I have the all property to unset stuff anyway.
  3. What I don't have is the ability to simply scope, as the spec' once promised, any component specific styles

Shadow DOM really does not help, because it creates an arbitrary barrier (except for custom properties, but that's not actually enough; values only, not declarations and no mixin support).

@Heydon

This comment has been minimized.

Copy link

Heydon commented Jan 28, 2019

Svelte, Vue, Angular but also styled-jsx (React) instead scope the styles to a single component to simulate ShadowDOM.

This does not simulate Shadow DOM. Shadow DOM prevents inheritance. The Vue behaviour (they actually emulate <style scoped> exactly) is more desirable than Shadow DOM.

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 28, 2019

Shadow DOM doesn't prevent inheritance (it used to in v1). eg. color crosses the SD boundaries https://jsfiddle.net/6ntd0cgw/

@o-t-w

This comment has been minimized.

Copy link
Author

o-t-w commented Jan 28, 2019

To be clear: shadow DOM does not prevent inheritance, but it does prevent global selectors *, p, etc from affecting the component. Therefor Svelte, Vue, Angular and styled-jsx are far more comparable to the scoped attribute than to shadow DOM. As Heydon said, Vue explicitly emulates <style scoped>

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 28, 2019

it does prevent global selectors *, p, etc from affecting the component

true

Therefor Svelte, Vue, Angular and styled-jsx are far more comparable to the scoped attribute than to shadow DOM. As Heydon said, Vue explicitly emulates <style scoped>

Not 100% sure about Vue (I think I checked once) and Angular but Svelte and styled-jsx act more like Shadow DOM. I am the co-author of styled-jsx and convinced Rich Harris (Svelte's creator) to switch from emulating <style scoped> to behave like styled-jsx.

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 28, 2019

update Vue also works like Svelte and styled-jsx https://codesandbox.io/s/ko4kp4nq5r https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles

Anyway this thread is not about frameworks I guess :) <style scope> affects the entire subtree and technically who wants to emulate this behavior could just use a unique id to scope styles to a subtree.

Eventually I don't mind if this makes it to the spec again but I don't think it is a proper solution the the lack of encapsulation in lite DOM.

@keithjgrant

This comment has been minimized.

Copy link
Contributor

keithjgrant commented Jan 28, 2019

In fact this is not different from prefixing every selector with a unique id eg. .foo { } becomes #scope-unique-id .foo {}.

I don’t think this is accurate. When scope was in the spec, it was defined such that inner scopes overrode outer scopes, regardless of selector specificity.

For example, given:

/* in one scope */
.foo p {
  color: blue;
}

/* in another scope */
.bar p {
  color: red;
}

A foo paragraph inside a bar would be blue, and a bar paragraph inside a foo would be red (assuming each is used in its own scope, nested). Which addresses precisely the problem CSS-in-JS seeks to solve, and would be hugely beneficial. Unfortunately, rules about scoping were removed from the Cascade working draft after the Scope spec got dropped.

I also thought the final version of the spec, which relied on a @scope at-rule rather than an html attribute was much better. (Here’s my write-up on that)

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 28, 2019

@keithjgrant thanks for your reply that's useful information! I never looked at the @scope proposal.

Correct me if I am wrong: when using the inline (html attribute) version would styles cascade? If yes even in the case of #foo p inside of a #bar b the former would be blue and vice versa. Unless you meant that in the "inline" version the inner scope always wins regardless of the specificity (I can imagine that being true for the at-rule version).

Either ways the problem I am talking about is that with this model and no lower boundaries (like in JS components or Shadow DOM) it is impossible to stop styles from leaking into the entire subtree. The perfect example is you are making a website using some templating system. A partial has a p that you didn't mean to style but @scope .foo { p { margin: 20px } } will match that and possibly break something (in your example you always reset but that's not always the case). Not sure if it is clear what I am trying to say.

@keithjgrant

This comment has been minimized.

Copy link
Contributor

keithjgrant commented Jan 28, 2019

Ah, yes. I see what you’re saying. I think you’re right.

The question of styles “leaking in” is an interesting one. Because I think in most cases, there are some style you would want to leak in, and others you wouldn’t.

Stepping back a bit, I think what I/we really want here is a first-class way to distinguish between base styles and “module” styles. Then you could define a bunch of base styles for the page (font family, color, default margins, etc.); these you would want to “leak in” to everything, as your default baseline. Then, on top of that, a way to define modular styles (module A has a blue heading, and some larger margins, module B has these borders, etc.); these you would want to scope only to their respective modules, and not leak in or out.

Perhaps these modular styles can be done via Shadow DOM, but without a set of base styles that do pierce into all modules, they come up lacking.

@fantasai

This comment has been minimized.

Copy link
Contributor

fantasai commented Jan 28, 2019

CSS Scoped Style spec proposal (@scope) which was removed due to lack of interest: https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#scope
Cascade rules for it: https://www.w3.org/TR/2018/CR-css-cascade-4-20180828/#cascading

@Heydon

This comment has been minimized.

Copy link

Heydon commented Jan 28, 2019

To be clear: shadow DOM does not prevent inheritance, but it does prevent global selectors *, p, etc from affecting the component. Therefor Svelte, Vue, Angular and styled-jsx are far more comparable to the scoped attribute than to shadow DOM. As Heydon said, Vue explicitly emulates <style scoped>

This is correct, I think (largely? Depends on the kind of properties?) But that makes it even worse, because it's so particular about what works and what doesn't. Yes: inheritance; no: global styles? When would you ever want to just inherit, but not have access to the global styles? Who thinks and works like that?

@Heydon

This comment has been minimized.

Copy link

Heydon commented Jan 28, 2019

CSS Scoped Style spec proposal (@scope): https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#scope

@fantasai This looks good promising and interesting. To be clear, is this a new proposal you recently wrote?

One concern is the limitation of using selector identifiers. Would this preclude from doing...

<custom-element>
   <div></div>
   <style scoped>
       div {
           max-width: 60ch;
       }
   </style>
</custom-element>

... where max-width: 60ch refers to just this instance of <custom-element>?

Sorry if I have understood it poorly.

@fantasai

This comment has been minimized.

Copy link
Contributor

fantasai commented Jan 29, 2019

@Heydon It's a proposal that was put together many years ago (the draft is dated 2014). It was dropped because nobody seemed to be interested in it. Work continued on Web Components, and the scoped style proposals were deleted from the specs.

Wrt your example, doing exactly that would require bringing back HTML scoped styles, which is a different syntax for triggering the same behavior as @scope. In either case, you're binding some styles to a scoping root: the HTML syntax implies that relationship by placing the <style> element inside the scoping root, and the CSS syntax uses a selector. (The downside to the HTML approach is that you have to inject the style sheet into the markup. Which maybe isn't too bad if you only have one instance, but it can be pretty repetitive if you have multiple instances with the same styles!) If you wanted to tie some @scope'd CSS rules to a particular instance of custom-element, it's possible: you could do it with an ID selector, for example. @scope binds to whichever elements are selected by the @scope, and if you somehow manage to select only that custom-element instance (whether using ID selectors or some other method that happens to select only that one element) then it only binds to that instance.

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 29, 2019

Yes: inheritance; no: global styles? When would you ever want to just inherit, but not have access to the global styles?

@Heydon good question! My take on this is that if you don't have global styles then probably inheritance should be opt-in too (as opposed to opt-out / status quo).

Who thinks and works like that?

I have spoken with many supporters of styles in JavaScript and apparently they all do this (CSS Modules folks included). I guess they adopt a different strategy to avoid repetition: instead of using global and cascading styles they build up isolated components and reuse those instead. When it is time to tweak styles they then use custom properties or (in JavaScript) "context" by making explicit subscriptions so that they control what is affected by those changes.

FWIW this proposal by Nicole Sullivan sounded interesting to me https://mobile.twitter.com/stubbornella/status/1076144685867982849

@keithjgrant The other day I was playing with another idea https://mobile.twitter.com/giuseppegurgone/status/1089633480458358785 but I am not sure how feasible it is without a way to mark DOM boundaries for the scope. This would be just an addition not a replacement of CSS.

@SelenIT

This comment has been minimized.

Copy link
Collaborator

SelenIT commented Jan 29, 2019

@fantasai, so does my assumption that the rules for the @scoped styles could possibly apply also to @nested selectors inside the style attribute (since the declarations from the style attribute are considered element-scoped) make sense or not?

@o-t-w

This comment has been minimized.

Copy link
Author

o-t-w commented Jan 29, 2019

<style scoped> affects the entire subtree and technically who wants to emulate this behavior could just use a unique id to scope styles to a subtree.

This is not the case. There are differences.
Obviously you can not reuse an id select. I will use a class instead in my example.

To take a React example:

const Profilecard = (props) => {
	
	const styles = `
	div {background-color: ${props.bgColor}}
	h2 { color: white; }
	img  { 
		border-radius: 50%;
		height: 80px;
		width: 80px;
	}`

	return (
		<div className="card">
			<style scoped>{styles}</style>
				<h2>{props.name}</h2>
				<img src="man3.jpg" alt=""/>
			</div>
	);
};

Here you could pass different props e.g. <Profilecard bgColor="blue"> would have a blue background and <Profilecard bgColor="red"> would have a red background.

Whereas with just using a class, styles will override each other:

const Profilecard = (props) => {
	
	const styles = `
	.card {background-color: ${props.bgColor}}
	.card h2 { color: white; }
	.card img  { 
		border-radius: 50%;
		height: 80px;
		width: 80px;
	}`

	return (
		<div className="card">
			<style>{styles}</style>
				<h2>{props.name}</h2>
				<img src="man3.jpg" alt=""/>
			</div>
	);
};

<Profilecard bgColor="blue"> and <Profilecard bgColor="red"> would both be red (or blue - depending on the order).

This API doesn't just account for scoping. It also accounts for two other things that have made CSS-in-JS popular:

  • the lack of needing to name things
  • being able to write CSS inside a Javascript file

See facebook/create-react-app#5224

@giuseppeg

This comment has been minimized.

Copy link

giuseppeg commented Jan 29, 2019

The solution you are describing above already exists for React https://github.com/zeit/styled-jsx

This is not the case. There are differences.
Obviously you can not reuse an id select. I will use a class instead in my example.

This part is not super clear to me but without a lower boundary (or to simply put it per component encapsulation) the styles would style nested components (in React children).

@fantasai

This comment has been minimized.

Copy link
Contributor

fantasai commented Jan 29, 2019

@SelenIT No, style attribute syntax doesn't allow selectors or at-rules. (The only reason style attributes are defined as "scoped" in that spec is to get the expected behavior--currently defined as "more specific than any selector". It's not introducing anything new.)

@chrishtr chrishtr added this to Backlog in High-priority issues via automation Jan 30, 2019

@Heydon

This comment has been minimized.

Copy link

Heydon commented Jan 30, 2019

@fantasai Thank you for the clarification! Some reflections:

The downside to the HTML approach is that you have to inject the style sheet into the markup. Which maybe isn't too bad if you only have one instance, but it can be pretty repetitive if you have multiple instances with the same styles!

My understanding is that this would be less repetitive than when writing CSS into a shadowRoot. Why? Because you can still draw upon already set global styles. Currently, when I write a custom element, the only easy way to use styles specific to an instance of that element is through Shadow DOM. And once I start using Shadow DOM, I have to play with its rules — including not being able to use global style rules.

@o-t-w

This comment has been minimized.

Copy link
Author

o-t-w commented Jan 30, 2019

The downside to the HTML approach is that you have to inject the style sheet into the markup. Which maybe isn't too bad if you only have one instance, but it can be pretty repetitive if you have multiple instances with the same styles!)

@fantasai
Do Constructible StyleSheets & adoptedStyleSheets change that situation at all?

@valtlai

This comment has been minimized.

Copy link

valtlai commented Jan 30, 2019

I couldn't accept the fallback behavior to be the scoped styles applying to the entire document

We could use type="scoped" (similar to <script type="module">).

@keithjgrant

This comment has been minimized.

Copy link
Contributor

keithjgrant commented Jan 30, 2019

Is there a reason to prefer the HTML attribute approach over a @scope rule in CSS? The latter seems to me to reduce code duplication and prevent poor fallback behavior.

@BrentARitchie

This comment has been minimized.

Copy link

BrentARitchie commented Jan 30, 2019

@keithjgrant That is a good question. Using the @scope rule would address some of the backwards compatibility and allow us more time to discuss the html attribute syntax.

body {
    background-color: red;
  }

@scope html {
  body {
    background-color: blue;
  }
}

The rule as written would give us a blue body background if the @scope rule is followed, but be coloured red if the rule isn't understood. Whereas, the scoped html attribute would simply act like an inline css declaration and make the body red anyways in all cases.

I think the @scope rule is more consistent with how the rest of the declarations work for backwards compatibility.

ie. If it is understood, use it. If it's not understood, just ignore it. Vendors have been touting progressive enhancement as the path of least resistence and for that to work, the language needs to be as consistent as possible. the scoped html attribute breaks that consistency in my opinion.

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