5

I'm beginning to use custom elements, and one thing I can't figure out is sharing styling. For example, if I have 2 custom elements, <element-1> and <element-2>, both of which contain <button>'s, and i want all buttons to have a certain styling, e.g. font-size:20px.

The options I've considered are:

  1. Use a <stylized-button> custom element instead of <button> in the custom elements. This is problematic when externally sourcing <element-1>. Also problematic if you want other styling as well (e.g. color:red) on only <element-1> buttons and not <element-2> buttons.

  2. As far as I could tell from polymer's docs [1], polymer doesn't have a solution for this either.

  3. /dead/ and :shadow seemed promising but are no longer supported.

  4. Similarly @apply [2] seemed promising, but the proposal was withdrawn.

  5. ::part and ::theme [3] seem even more promising, but aren't yet supported.

  6. Use js to support ::part and ::theme [4]. i imagine this would be very brittle without ironing out all cases.

  7. Explicitly add the shared styling to each custom element.

     class Element1 extends HTMLElement {
         constructor() {
             this.shadowRoot.addElement(sharedStyle);
         }
     }
    

    This seems very restricted & manual. Also might affect performance? Also problematic if you externally sourcing <element-1>.

Right now, I'm thinking #6 might be the best as it seems the most generic / easiest to use without building specifically for it, plus it would make transitioning to #5 trivial when it's implemented. But I'm wondering if there are other approaches or suggestions?

[1] https://www.polymer-project.org/3.0/docs/devguide/style-shadow-dom

[2] http://tabatkins.github.io/specs/css-apply-rule/

[3] https://meowni.ca/posts/part-theme-explainer/

[4] A naive implementation and an example using it: https://gist.github.com/mahhov/cbb27fcdde4ad45715d2df3b3ce7be40

implementation:

document.addEventListener('DOMContentLoaded', () => {
    // create style sheets for each shadow root to which we will later add rules
    let shadowRootsStyleSheets = [...document.querySelectorAll('*')]
        .filter(element => element.shadowRoot)
        .map(element => element.shadowRoot)
        .map(shadowRoot => {
          shadowRoot.appendChild(document.createElement('style'));
          return shadowRoot.styleSheets[0];
        });

    // iterate all style rules in the document searching for `.theme` and `.part` in the selectors.
    [...document.styleSheets]
        .flatMap(styleSheet => [...styleSheet.rules])
        .forEach(rule => {
          let styleText = rule.cssText.match(/\{(.*)\}/)[1];

          let match;
          if (match = rule.selectorText.match(/\.theme\b(.*)/))
            shadowRootsStyleSheets.forEach(styleSheet => styleSheet.addRule(match[1], styleText));
          else if (match = rule.selectorText.match(/\.part\b(.*)/))
            shadowRootsStyleSheets.forEach(styleSheet => styleSheet.addRule(`[part=${match[1]}]`, styleText));
        });
  });

and the usage:

<style>
  .my-element.part line-green {
    border: 1px solid green;
    color: green;
  }

  .theme .line-orange {
    border: 1px solid orange;
    color: orange;
  }

  /*
    must use `.part` instead of `::part`, and `.theme` instead of `::theme`
    as the browser prunes out invalid css rules form the `StyleSheetList`'s. 
  */
</style>

<template id="my-template">
  <p part="line-green">green</p>
  <p class="line-orange">orange</p>
</template>

<my-element></my-element>

<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      const template = document.getElementById('my-template').content.cloneNode(true);
      this.shadowRoot.appendChild(template);
    }
  });
</script>
junvar
  • 11,151
  • 2
  • 30
  • 46

2 Answers2

1

You can use @import url to import an external stylesheet into different custom elements.

Alternately now you can also use <link rel="stylesheet"> inside a custom element Shadow DOM:

<template id="element-1">
  <style> 
      @import url( 'button-style.css' )
  </style>
  <button>B-1</button>
</template>

<template id="element-2">
  <link rel="stylesheet" href="button-style.css">
  <button>B-2</button>
</template>
Supersharp
  • 29,002
  • 9
  • 92
  • 134
  • this seems equivalent to #7, and follows the same downsides; e.g. if element-2 is defined by a dependency, I won't be able to style its buttons with this method. But I am actually trying this on one project (where all the elements are mine), and it's working fine. – junvar Oct 31 '18 at 21:47
  • i don't understand what you mean with depedency, a third-party custom-element? yes it's a limitation but it's precisely the purpose and expected behavior of the shadow dom features. – Supersharp Oct 31 '18 at 21:59
  • Yes, that's what i mean by dependency, though it may not be a 3rd party but instead your own generic bundle of elements you'd like to reuse with different styling. I disagree that that's the "purpose and expected behavior", it's more of lag in implementation. There's definitely been attempts to support this with shadow elements in the past (e.g. /deep/, :shadow, and @apply), and there's definitely plans to support in future (e.g. ::part and ::theme), which is being actively worked on by chrome at least. – junvar Nov 01 '18 at 12:54
  • For example, imagine you couldn't set the background color of buttons. Most pages, including stackoverflow, would look much different or require implementing multiple custom buttons. If anything, the shadow dom and web components make things modular and easier to reuse and must therefore allow being as generic and configurable as typical html elements. – junvar Nov 01 '18 at 12:57
  • 3
    The differents attempts have failed for multiple reasons (slow, impossible to implement, dangerous...). ::part and ::theme won't work with 3rd party libraries and are not coming very fast (still no alpha release). Custom properties (or custom implementations) are the only alternatives (with the same or more limitations as in the answer). That's why stating the CSS isolation is the purpose of Shadow DOM is realistic. If you want global styling I'd recommend to design Custom Elements without Shadow DOM. They work well too and are easier to polyfill. – Supersharp Nov 01 '18 at 15:36
  • 1
    Interesting suggestion, I've found few resources online using custom elements without shadow dom, and so I had assumed it's bad practice or something. I will give it a try for my use case and see if any problems arise. – junvar Nov 01 '18 at 16:33
  • @Supersharp thank you for clearing my doubts. I have a lot of beef with Shadow DOM at the moment, because it doesn't make me productive delivering website to client. Not only that client may be not familiar with Custom Elements, but if I just inlined same reset stylesheet into each component (resulting in a bundler requirement), it would drive them away as well as myself from adopting web components and DRY. I'll just stick to my guns and do without Shadow DOM - after all, these are just bunch of home-brew UI kit components that are not required to be reusable. – vintprox Nov 18 '22 at 18:24
-1

If you are using css, you can just do this:

button {

  /* Put Style Here */  

}

You also have to add a link in the head on html:

<link rel=“stylesheet” href=“the address”>
object-Object
  • 1,587
  • 1
  • 12
  • 14