Reputation: 5891
I have a custom element (without shadow DOM) that I'd like to be able to use anywhere, even inside another custom element that might use shadow DOM. However, I'm not sure how to get the styles working in both places.
For example, lets say I create a simple fancy-button
element:
class fancyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<style>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<button>Click Me</button>`;
}
}
customElements.define('fancy-button', fancyButton);
<fancy-button></fancy-button>
Inside a shadow DOM element, the inserted style tag will allow the fancy-button
styles to work. However, if this component gets used outside of a shadow DOM element, the style tag will be duplicated every time the element is used.
If instead I add the style tag as part of the html import file, then the styles only work outside of the shadow DOM but at least they are only declared once.
<!-- fancy-button.html -->
<style>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<script>
class fancyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `<button>Click Me</button>`;
}
}
customElements.define('fancy-button', fancyButton);
</script>
What's the best way to add custom element styles that handles both being used inside and outside the shadow DOM?
Upvotes: 9
Views: 9655
Reputation: 5891
So I was able to find a solution thanks to Supersharp suggestions about checking if we're in the shadow DOM.
First you add the styles as part of the import file so that the styles apply outside of the shadow DOM by default. Then when element is added to the DOM, we check getRootNode()
to see if it's been added to a ShadowRoot
node. If it has, and the styles haven't already been injected into the root, then we can inject the styles manually.
var div = document.createElement('div');
var shadow = div.attachShadow({mode: 'open'});
shadow.innerHTML = '<fancy-button></fancy-button>';
document.body.appendChild(div);
<style data-fs-dialog>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<script>
class fancyButton extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<button>Click Me</button>`;
var root = this.getRootNode();
// In polyfilled browsers there is no shadow DOM so global styles still style
// the "fake" shadow DOM. We need to test for truly native support so we know
// when to inject styles into the shadow dom. The best way I've found to do that
// is to test the toString output of a shadowroot since `instanceof ShadowRoot`
// returns true when it's just a document-fragment in polyfilled browsers
if (root.toString() === '[object ShadowRoot]' && !root.querySelector('style[data-fs-dialog]')) {
var styles = document.querySelector('style[data-fs-dialog]').cloneNode(true);
root.appendChild(styles);
}
}
}
customElements.define('fancy-button', fancyButton);
</script>
<fancy-button></fancy-button>
When all browsers support <link rel=stylesheet>
in the shadow DOM, then the inline script can turn into an external stylesheet as robdodson suggested, and the code is a bit cleaner.
Upvotes: 7
Reputation: 6786
You'll probably want to put the styles in a separate CSS file that you vend along with your element's JS. But as you've pointed out, if you put the element inside the Shadow DOM of another element then the styles won't work in that scope. For this reason it's usually best to just create a shadow root and pop your styles in there. Any reason why you wouldn't want to do that?
Upvotes: 3