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

Style isolation through Shadow DOM #22

Closed
rauchg opened this issue Oct 16, 2016 · 38 comments
Closed

Style isolation through Shadow DOM #22

rauchg opened this issue Oct 16, 2016 · 38 comments

Comments

@rauchg
Copy link
Member

rauchg commented Oct 16, 2016

For the vast number of users who with very good reasons reject CSS-in-JS, we could support something like this:

import Head from 'next/head'
import shadow from 'next-shadow'
export shadow(() => (
  <footer>
    <div>hi <span>world</span></div>
    <style>{`
      div span {
        /* this won't conflict with any other element anywhere,
            including spans from nested components */
        color: red
      }
    `}</style>
  </footer>
))

In the future, if we find a way of making this work with server-rendering, we could make every single component a custom element with built-in isolation capabilities out of the box, no shadow() wrapping needed. If you want global effects, you can use <Head>, otherwise everything is isolated.

This would put CSS-in-JS and full CSS support on equal ground, both supported, both with pros-and-cons and great performance.

The problems:

  • Limited browser support for now
  • Is this actually doable?
    • Perhaps base it on react-shadow
    • What's the server-rendering story of custom elements? Do they require JS execution for the elements registration? Could be slow and impractical
  • Are there performance drawbacks?
@orta
Copy link
Contributor

orta commented Oct 25, 2016

Could this work with styled-components? https://styled-components.com

@Munter
Copy link

Munter commented Oct 25, 2016

Styled-components will indeed do exactly this

@giuseppeg
Copy link
Contributor

giuseppeg commented Oct 26, 2016

hey @rauchg here is a codepen of what I was talking about yesterday at the React Vienna meetup.

As you can see it is not perfect and probably server-side rendering is not an option – SSR should fallback to some other solution I am afraid or serve a skeleton that gets enhanced on the client.

There is an old PR where Pete Hunt made a POF for adding ShadowDOM support.

Styled components are nice but imho the codebase is a bit too heavy and, as I said yesterday, they don't provide full styles encapsulation like ShadowDOM does – inheritance still applies.
Same for CSS Modules, BEM etc.

You may want to take a look at CSJS which has the same "problems" but it is quite lightweight.

FWIW we added an experimental encapsulation feature to suit css – tl;dr – maybe it could inspire some other work.

It is not perfect but I believe that in CSS-in-JS it'd be more robust – in fact we might add a similar feature to JSS.

@kof
Copy link

kof commented Oct 26, 2016

I also need to add that JSS has already inheritance isolation using jss-isolate plugin. It isolates all rules by default but allows to avoid isolation on per-rule basis. This is basically what Pete Hunt promoted recently.

@rauchg
Copy link
Member Author

rauchg commented Oct 26, 2016

@orta @Munter styled-components should already work!

To clarify, the "dream" here is as follows. You write components/button.js:

export default ({ text }) => (
  <button>{ text }</button> 
  <style>{`
    button {
      border: 1px solid red;
      background: #fff;
      padding: 10px;
    }
  `}</style>
)

The idea here is that the <style> only applies to the elements defined within that component. No CSS-in-JS, no learning curve, no transformations. No global leaks.

Now: what happens if you instantiate multiple <Button />? Does each get its own <style>? Not sure how custom elements handle re-use, but that's why maybe the API will have to be something like this instead:

   export default shadow(
     () => (
       <button/>
     ),
     <style>{`
       /* … */
     `}</style>
   )

Also, the server-render story of custom elements / shadow dom is also a big question mark for me.

@dstreet
Copy link
Contributor

dstreet commented Oct 28, 2016

Perhaps I'm missing something here, but couldn't you use a higher-order component that would assign a unique id to its rendered component and encapsulate within the scope of the unique id?

const MyComponent = () => (
  <button>{ text }</button>
  <style>{`
    button {
      border: 1px solid red;
      background: #fff;
      padding: 10px;
    }
  `}</style>
)

export default StyleWrapper(MyComponent)

Rendering something like:

<div id="asdf1234">
  <button>Click me</button>
  <style>
    #asdf1234 {
      button {
        border: 1px solid red;
        background: #fff;
        padding: 10px;
      }
    }
  </style>
</div>

@threepointone
Copy link
Contributor

glamor itself has work in progress to allow 'real' inline css, taking support for syntax highlighting and linting from styled components' work.

https://twitter.com/threepointone/status/790268053124632576/

the advantage of using glamor for the same, is we can optionally 'precompile' the source with a babel plugin and eliminate the need for css parser in the bundle / runtime.

https://twitter.com/threepointone/status/791232832865603584/

all glamor goodies would still apply - SSR, nesting, etc.

@giuseppeg
Copy link
Contributor

giuseppeg commented Oct 29, 2016

@threepointone inheritance still applies right? If so it is still missing one feature from ShadowDOM that is reset inherited values at the root of the component.

@threepointone
Copy link
Contributor

@giuseppeg yes, you're right. this isn't proposed as an alternative to SD, but more as an alternative 'css' syntax to glamor.

@mxstbr
Copy link
Contributor

mxstbr commented Oct 31, 2016

Just to chime in here

the advantage of using glamor for the same, is we can optionally 'precompile' the source with a babel plugin and eliminate the need for css parser in the bundle / runtime.

Working on this for styled-components over the next two weeks, stealing some of glamors ideas 😉 (related issue)

@giuseppeg
Copy link
Contributor

If so it is still missing one feature from ShadowDOM that is reset inherited values at the root of the component.

Sorry this is inaccurate, see similar issue.

@kevinSuttle
Copy link

@rauchg
Copy link
Member Author

rauchg commented Nov 3, 2016

@dstreet that's an interesting solution. I'll add that the main problem it has is that it still requires us to parse the entire CSS grammar to prefix all the selectors with that ID.

It also introduces some unexpected side effects.

If we wrap with <div>, the following would be rendered as a block:

module.exports = ({ text }) => <span><b>text</b></span>

so it would have to be <span>, but I'm pretty sure that assuming that every component can be wrapped with <span> is likely to have some unforeseen issues.

Also, the issue of not being able to "server-render" shadow DOM still stands. If we merely prefix, child elements of child components would get unexpected styles

@rauchg
Copy link
Member Author

rauchg commented Nov 3, 2016

@threepointone that's the most solid solution right now. We might include that babel extension by default, or have people extend babel to add it themselves in package.json. The former might not be too intrusive since we already include glamor anyways (unless it introduces a prohibitive performance degradation for non-glamor users)

@BLamy
Copy link

BLamy commented Nov 9, 2016

Here's is a webpack plugin for css literals.

const styles = css`
  .button {
    color: black;
    border: 1px solid black;
    background-color: white;
  }
`;

const Button = () => <button className={styles.button} />;

https://github.com/4Catalyzer/css-literal-loader

@kevinSuttle
Copy link

@rauchg Here you go: webcomponents/shadycss#3

@thysultan
Copy link
Contributor

someone may find this useful. here's how i did it with scoping.

http://jsbin.com/jeweqis/7/edit?js,output

impl details

it works by providing a .stylesheet method similar to reacts .render method with the exception that the return value is a string of css that is parsed and cached.

@pemrouz
Copy link

pemrouz commented Nov 13, 2016

@rauchg fwiw here's how I addressed these problems in Ripple:

  • The standard ripple.draw just invokes the component on a node.
  • The shadow module makes all components isolated with shadow boundaries by default. It creates a shadow root if the browser supports it and there isn't one already, and then continues with the next render middleware. However instead of return next(el), it changes the root to be the shadow root rather than the host: return next(el.shadowRoot).
  • SSR works because any server rendered content is moved into the shadow root when the shadow root is created for the first time. The first render on the client is still a no-op and the original output looks good to crawlers.
  • In presence of shadows, stylesheets are placed with shadow roots. This is liberating as you can just focus on writing isolated styles by default. If there are no shadows, it dedupes and places them in the head. There are two issues with this: the "upper boundary" and "lower boundary" problem (as you alluded to). The upper boundary is solved simply by prefixing styles with the custom element name (no wrapping, no random id's - easy to do as components in ripple are web components). The lower boundary is the only one new thing that cannot be worked around. The best solution I've found is to not be greedy by default. Use the child selector over the descendant selector. Components usually have a known structure, it's very rare you actually want to say match from here to infinity. For example, from the styles in d3-chosen you have something like:
:host { }
:host > .dropdown { }
:host > .dropdown > li { }

Using over> is perhaps just our default habit as that is what is easier to learn.

@robdodson
Copy link

robdodson commented Nov 15, 2016

@rauchg

what happens if you instantiate multiple <Button />? Does each get its own <style>?

Yes I believe each shadow root gets its own <style>. That's what the Polymer project does when it creates elements. I spoke with a member of the Blink team about the cost and he said:

As long as the text of the style element is identical (literally everything down to whitespace) it's pretty cheap

It's mostly the cost of creating and appending and element and then some hash table lookups

My take is that the actual styles get shared (hence the hash table lookup). So I don't think this should be a performance concern.

Regarding the other question:

What's the server-rendering story of custom elements? Do they require JS execution for the elements registration? Could be slow and impractical

Custom Elements have lifecycle callbacks for when they're constructed, connected/disconnected from the DOM, and anytime their attributes change. On the server you'd probably want to ensure that these lifecycle callbacks get triggered. There's a library that does this today called server-components.

I guess a next.js element could choose to ignore these callbacks if you have your own system for bootstrapping a component and rendering its template, but someone may include a vanilla js custom element in their app and you might want that element to stamp its template server side as well.

cc @treshugart

@rauchg
Copy link
Member Author

rauchg commented Nov 15, 2016

@robdodson it's still not clear to me what the DOM markup string that the server outputs looks like for shadow DOM / CSS?

@rauchg
Copy link
Member Author

rauchg commented Nov 15, 2016

btw thanks everyone for your insight, super interesting thread!

@treshugart
Copy link

treshugart commented Nov 16, 2016

it's still not clear to me what the DOM markup string that the server outputs looks like for shadow DOM / CSS?

This is non-trivial. I commented on a Polymer issue relating to that. There's also a w3c issue that's been closed due to no concrete proposals for declarative shadow roots.

Basically the major things that need to be answered are:

  1. how do you represent a shadow root in HTML?
  2. how do you represent the default content for a <slot />?
  3. how do you represent slotted content for a <slot />?
  4. how do you get the styling to apply to both the non-initialised shadow root (so, server-serialised) and the initialised shadow root?
  5. how do you then go about rehydrating the shadow roots? This involves attachShadow() and moving the declared HTML into it and then figuring out slot default content / slotted nodes and moving the slotted nodes to light DOM and then default content into the actual <slot /> nodes.

Number 4 may be moot if we can assume 5 is sync as the nodes are rendered. If this was pure web components, custom element definitions can be loaded async. It could just be a requirement of SSR web components to assume all definitions are loaded before the HTML is piped.

@rws-github
Copy link

I basically can't use Next.js because it doesn't support traditional CSS. My projects have to use existing CSS files and having consistent class names does make browser-based testing easier.

I really just want:

<div className='myStyle'>...

@nkzawa
Copy link
Contributor

nkzawa commented Nov 16, 2016

@rws-github actually, you can use traditional CSS too, by loading style files through Head component. You have to load global styles on all pages though.

export default () => (
  <div>
     <Head><link rel="stylesheet" href="/static/styles.css"></Head>
     <div className="myStyle">hi</div>
  </div>
)

@rws-github
Copy link

@nkzawa Thank! That worked.

@pemrouz
Copy link

pemrouz commented Nov 16, 2016

@robdodson, @treshugart: I think it might be better to differentiate between "rendering a web component in next.js" (that potentially uses all the functionality like slots etc) with just "adding style encapsulation for next.js components". The former scope is relatively a lot bigger, will be very hard to reliably and without bloat.

it's still not clear to me what the DOM markup string that the server outputs looks like for shadow DOM / CSS?

@rauchg: just as it is today without shadow roots! Consider the HTML output today a baseline that works everywhere, and the Shadow DOM on the client as a progressive enhancement if available. This approach also dovetails pretty well in a unidirectional architecture with declarative components.

@kevinSuttle
Copy link

@pemrouz

Without bloat

Not sure what you're referring to here. You don't have to go all in on WC. Just Shadow DOM webcomponents/shadycss#3

@pemrouz
Copy link

pemrouz commented Nov 16, 2016

Not sure what you're referring to here. You don't have to go all in on WC. Just Shadow DOM webcomponents/shadycss#3

Yep, that was my point :) There's some low hanging WC fruits (e.g. invoking connectedCallback on server-side), but might be easier to just focus on one thing at a time (Shadow DOM and style encapsulation).

ShadyCSS looks pretty good. The API doesn't quite make sense to me though: Why is limited to a <template> element? How would you apply styles to a component without a <template>? Also (with react) it looks like you may need to apply/updateStyles in every render, which would be too slow? Can the other functionality (e.g. custom properties) be separated from scoping?

I use cssscope which does one thing: scopes a stylesheet (upper boundary only). This is then lazily deduped by content hash and inserted into head when the component is first used.

const scope = require('cssscope')
    , styles = `
        :host { }
        :host > .dropdown { }
        :host > .dropdown > li { }`
    , scoped = cssscope(styles, 'd3-chosen')

// scoped:
// d3-chosen { }
// d3-chosen > .dropdown { }
// d3-chosen > .dropdown > li { }

@rauchg
Copy link
Member Author

rauchg commented Nov 16, 2016

@rws-github in general we advocate you use next/css because of isolation (your class names will never collide or compete between components). But you can still always include global CSS. More here: https://github.com/zeit/next.js/wiki/Global-styles-and-layouts

@rauchg
Copy link
Member Author

rauchg commented Nov 16, 2016

@pemrouz this is what I have in mind right now for a "Shadow CSS" approach, that can later be only used for backwards compatibility once we have the appropriate markup for server-rendering shadow dom

#249

@treshugart
Copy link

@pemrouz

@robdodson, @treshugart: I think it might be better to differentiate between "rendering a web component in next.js" (that potentially uses all the functionality like slots etc) with just "adding style encapsulation for next.js components". The former scope is relatively a lot bigger, will be very hard to reliably and without bloat.

Agreed, I was just responding within the context of the original issue.

@pyrossh
Copy link

pyrossh commented Nov 19, 2016

@treshugart Are you planning to make something like nextjs for skatejs? Then it would really easy to adopt?

@treshugart
Copy link

@pyros2097 it's likely something will happen at some point (open issue here for tracking it. I'd prefer a generic solution for web components, though. Making something Skate specific wouldn't help the rest of the community.

I don't want to derail from this conversation. Happy to discuss in Skate's issues :)

@traviskaufman
Copy link

FWIW angular2 has a nice strategy for a similar problem using ViewEncapsulation. It would probably have to be modified a bit to fit less structured usage, but perhaps worth looking into

@rauchg
Copy link
Member Author

rauchg commented Nov 26, 2016

@traviskaufman does angular have support for server-rendering components with isolated styles? That's a requirement for Next.js

@thysultan
Copy link
Contributor

So i built stylis.js, that when given a string and namespace it returns css that is isolated to that namespace and prefixed, it's low level so you might be able to build an abstraction upon it.

@traviskaufman
Copy link

@rauchg I believe so. angular/universal render's components server-side and it uses the same style system as angular2's browser platform.

@rauchg
Copy link
Member Author

rauchg commented Dec 19, 2016

I want to revisit this when web components have a server-rendering story (WICG/webcomponents#71). For now, styled-jsx gives us the best degree of support of "regular CSS" in a component-friendly, SSR-friendly way.

@rauchg rauchg closed this as completed Dec 19, 2016
@lock lock bot locked as resolved and limited conversation to collaborators May 12, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests