Override styles in a shadow-root element
Asked Answered
W

10

78

Is there a way to change styles found in a shadow element? Specifically, extend/overwrite some properties found in a css class? I am using a chrome-extension called Beanote which hasn't been updated since April(2017) and there's a pesky bug I'd like to fix. I found that one line of css patches it up enough for me, but I am at a loss at applying it without going inside of the shadow element itself and directly editing those styles in the dev tools.

I'm looking for a way for this:

/*global css rule*/
.the-class-name { property-name: my-value; }

to overwrite this:

/* style tag inside the shadow-root */
.the-class-name { property-name: bad-value; }


Most of the resources I've found online with queries involving shadow-root override style or edit shadow-root styling had something to do with :host which, if its meant for this, doesn't work for my needs or deprecated functionality like ::shadow.

Waldman answered 4/12, 2017 at 0:49 Comment(0)
A
94

Because of the isolation of styles, which is a feature of Shadow DOM, you cannot define a global CSS rule that will be applied in the Shadow DOM scope.

It could be possible with CSS variables but they should be implemented explicitly in the shadowed component (which is not the case with this 3rd party library).

A workaround is to inject the line of style in the shadow DOM directly.

//host is the element that holds the shadow root:
var style = document.createElement( 'style' )
style.innerHTML = '.the-class-name { property-name: my-value; }'
host.shadowRoot.appendChild( style )

NB: it will work only if the Shadow DOM mode is set to 'open'.


2019 update for Chrome 73+ and Opera 60+

Now it is possible to instantiate a CSSStyleSheet object directly and to affect it to a Shadow DOM or a document:

var sheet = new CSSStyleSheet
sheet.replaceSync( `.color { color: pink }`)
host.shadowRoot.adoptedStyleSheets.push(sheet) // You want to add to stylesheets list

Nothe though to not add the same style sheet multiple times to adoptedStyleSheets array, e.g. at reloading a SPA pages.

Anaesthesiology answered 4/12, 2017 at 12:15 Comment(5)
If there are other ways around it, I'd definitely go for that. But this helped exactly enough to get the bug patched out. If anyone wants to see the userscript its on gist under anonymous/tamper_monkey_beanote_patch.js.Waldman
That is doable, but quite a hack. What would be a proper way when somebody needs to extend shadow root style of an ionic component? Unless all components integrate all css attributes as variables, which we could control from outside, we need a way to extend styles. Such as extending component's original css file. How do we extend ionic's button.css for example, with new variables?Indihar
you cannot define a global CSS rule that will be applied in the Shadow DOM scope --- actually, you can... any rule that applies to the host element is inherited by the shadow tree elements. e.g. put div { color:red; } in the main css, then add a shadow tree under a div... the divs inside the shadow tree will also be red.Clippers
@Clippers No it's not the sames thing, just an illusion: the global rule won't be applied to the inner divs. However the color property will be inherited because it's its default value.Anaesthesiology
You can style named elements within the shadow tree using ::part(name). See developer.mozilla.org/en-US/docs/Web/CSS/::partThralldom
S
28

Ionic V4 select down icon color change example

document.querySelector('#my-select').shadowRoot.querySelector('.select-icon-inner').setAttribute('style', 'opacity:1');


ionViewDidEnter() {
    document.querySelector('#my-select').shadowRoot.querySelector('.select-icon-inner').setAttribute('style', 'opacity:1');
  }

If you want overwrite the default generated shadowRoot style then have to call js function after page loaded fully.

Subjunctive answered 24/2, 2019 at 18:27 Comment(1)
Thnx, I used this approach for overriding the swiper styles in the Angular project.Stack
C
6

Extending on the previous answers.

Outside styles always win over styles defined in the Shadow DOM, i.e. when you add a global style rule that reference the component you are styling. See: https://developers.google.com/web/fundamentals/web-components/shadowdom#stylefromoutside

Otherwise this will depend on if the elements shadow DOM was embedded with a styleSheet, or if it adopted a style-sheet using adoptedStyleSheets.

If the element was embedded in the element you can add or insert a rule to the existing style-sheet using addRule or insertRule. This also work for style-sheets added with adopedStyleSheets.

As mentioned in the previous answer, you can append a new style-sheet to the list of adopted style-sheets instead. This also work when the shadowRoot contains a embedded styleSheet, since adoptedStyleSheets takes precedence, and styleSheetList is a read-only property.

assert(myElement.shadowRoot.styleSheets.length != 0);
myElement.shadowRoot.styleSheets[0].addRule(':host', 'display: none;');

assert(myElement.shadowRoot.adoptedStyleSheets.length != 0);
`myElement.shadowRoot.adoptedStyleSheets[0].addRule(':host', 'display: none;');`

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: none; }`);

const elemStyleSheets = myElement.shadowRoot.adoptedStyleSheets;

// Append your style to the existing style sheet.
myElement.shadowRoot.adoptedStyleSheets = [...elemStyleSheets, sheet];

// Or if just overwriting a style set in the embedded `styleSheet`
myElement.shadowRoot.adoptedStyleSheets = [sheet];
Cholla answered 21/6, 2019 at 16:0 Comment(0)
P
3

A solution for cases when you have nested elements with shadowRoot.

Imagine you have:

<div class="container">
  #shadowRoot
    <div class="inner-component">
      #shadowRoot
        <div class="target-element">some</div> <!-- gonna be color:red -->
    </div>
</div>

Usage:

const topLevelComponentSelector = '.container';
const innerComponentSelector = '.inner-component';
const cssToAdd = '.inner-component > * { color: red }';

adjustShadowRootStyles([topLevelComponentSelector, innerComponentSelector], cssToAdd);

The implementation (Typescript):


//main function
function adjustShadowRootStyles(hostsSelectorList: ReadonlyArray<string>, styles: string): void {
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(styles);

  const shadowRoot = queryShadowRootDeep(hostsSelectorList);
  shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sheet];
}

// A helper function
function queryShadowRootDeep(hostsSelectorList: ReadonlyArray<string>): ShadowRoot | never {
  let element: ShadowRoot | null | undefined = undefined;

  hostsSelectorList.forEach((selector: string) => {
    const root = element ?? document;
    element = root.querySelector(selector)?.shadowRoot;
    if (!element) throw new Error(`Cannot find a shadowRoot element with selector "${selector}". The selectors chain is: ${hostsSelectorList.join(', ')}`);
  });

  if (!element) throw new Error(`Cannot find a shadowRoot of this chain: ${hostsSelectorList.join(', ')}`);
  return element;
}

P.S. Keep in mind that such webcomponents might appear with a small delay or async, so you have to call adjustShadowRootStyles() after they were loaded.

Or just wrap it with setTimeout, e.g. setTimeout(() => adjustShadowRootStyles(...), 60). Hacky but works.

Pareira answered 25/11, 2022 at 13:44 Comment(0)
V
2

All the answers that suggest using new CSSStyleSheet() should be taken with a grain of salt, as of today (almost 2023, happy new year everyone!) Safari STILL does not support the stylesheet constructor, that means your code won't work on ANY iOS device as every browser you can have there is forced to use Apples Webkit engine (yes, even if you download an app that says "Chrome"!). So if we use this (proper) approach to inject styles into a shadowRoot, it will fail on iOS devices, which is not acceptable for my standards, unfortunately...

https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet

Voracity answered 30/12, 2022 at 14:12 Comment(3)
is there a workaround then?Bone
developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/… .. seems like ios/safari 16.4 is supportedPassible
Thanks for the update, seems as of April 2023 Apple has caught up with the rest of the world!Voracity
W
1

I'd like to second an answer given by @Renato in one of the comments, since it points out the good, IMHO, way to solve the problem of customizing a WebComponent from the hosting application.

@Supersharp is right in the fact, that the external CSS rules are not propagating into thee Shadow Root, that's by design.

CSS variables are a good direction, but from my personal experience are a bit of an overkill for a value of a singular usage, AND yes, they MUST be supported be the WebComponent up-front.

Propagating the properties through the :host via inheritance (exactly as @Renato mentioned) is, IMHO, the perfectly right pattern aligned with the API design:

  • Custom element's (:host's) CSS rules are by design overridable by the outer rules
  • :host's children, the inner content of the Shadow DOM, MAY inherit the CSS rules of the :host, either by default or by explicit rule - and this is too, by design

I'd say, that where applicable, this approach would better be taken before considering CSS stylesheet injection, and also does not suffer from the limitation of open mode only.

Of course, this approach won't help when:

  • Inner elements are not inheriting relevant rules from the :host
  • The structure of a WebComponent is quite complex, so that single :host simply can't help them all

Yet, again from my own experience, simple components with desirably overridable CSS rules may benefit much from the non-intrusive pattern of propagating rules via :host.

Workbook answered 7/7, 2019 at 13:59 Comment(8)
Do you mean, that something like :host {color:blue} works only because in the case of color it propagates to be inherited by the elements in the shadow root, while with other properties this isn't the case? So what do we do when properties do not get inherited by shadow root's children? I mean, in that case we can only add new style sheets. But that's so not declarative.Anomie
Right, and as I've mentioned - where applicable. And yes - if I can't change the component implementation, but must change its behavior - than style injection is the way. You right - not nice, and I'd even go further - if its not designed that way, I better go find another one, cause it is just not maintainable that way.Workbook
True, I agree about the maintainability and it not being ideal, but I feel as if this particular part of Web Components APIs makes it by default that component authors in general will fail to make their part styles easily overrideable unless they put lots of effort into it (naming all parts, or coming up with some style system like the one in the material-ui.com component library for React, which allows much style flexibility, but makes the pattern very unique to that particular component library and ubiquitous).Anomie
Not sure what you've meant by "this particular part makes it by default", but to be sure - seems to me like we are in general agreement, when saying "go find another one" i've not meant WC, but the specific component! That is to say, similarly to you I hope, that it is nowadays a requirement from any component author to design the component with extensibility in mind, locking down the private stuff for protection (Shadows is the tool here), but opening up the customization as much as possible (CSS variables, :part, or, at some points, inheritance rules), being style injection last resort.Workbook
Yep. Agreed. It's a big shift going from writing plain HTML with CSS styling, to writing CEs with styling for end usersto override. The amount of work grows too much. Wishing there was a system that made extension of styles easy. Maybe that's what shadow root piercing styles were for before they were removed from spec. Is there another (at least theoretical) way to make it easy for CE users to override styles without CE authors having manually expose everything? If you look at built-in element shadow doms, vendors painstakingly expose everything with pseudo="" (similar to part="").Anomie
I suppose manual exposing is good to ensure public contracts. It's just a big step from using no CEs to using CEs, and not a very clear step at that.Anomie
:) I've almost say to myself - "good, we've conclude this discussion", and this morning suddenly I thought to throw in another API-like possibility: component authors may document the classes/structure of extensible parts of the CE/WC internal implementation and provide an inject API for a consumer to supply the customization stylesheet. This can be more convenient and less fragile approach due to being it less fragmented than dozen of custom variables or parts and allow easy reuse of some CSS fragments that might be used also elsewhere in the shell application. WDYT? (& let's end :) )Workbook
I think so. And it seems adoptedStyleSheets is for that purpose! :)Anomie
P
1

With web components you often want to copy all ancestors stylesheets to your target component:

const cloneStyleSheets = element => {
  const sheets = [...(element.styleSheets || [])]
  const styleSheets = sheets.map(styleSheet => {
    try { 
      const rulesText = [...styleSheet.cssRules].map(rule => rule.cssText).join("")
      let res = new CSSStyleSheet()
      res.replaceSync(rulesText)
      return res
    } catch (e) {
    }
  }).filter(Boolean)
  if (element===document) return styleSheets
  if (!element.parentElement) return cloneStyleSheets(document).concat(styleSheets)
  return cloneStyleSheets(element.parentElement).concat(styleSheets)
}

Then in your component:

constructor () {
  super() 
  this.attachShadow({mode: 'open'})
  this.shadowRoot.adoptedStyleSheets = cloneStyleSheets(this)
Passible answered 4/4, 2023 at 0:9 Comment(0)
D
1

When you need to add more that one line of CSS to a (e.g. nested) web component, the more convenient way is to take the CSS rules from the already imported CSS sheet. So that in your HTML import a CSS:

<!-- e.g. index.html -->
<link rel="stylesheet" id="styles-file-for-custom-element" href="./styles.css">

Then add the CSS rules to a web component via JS:

// Somewhere in your JS, primitivised for brevity
window.addEventListener('load', addCSSFromSheet);

function addCSSFromSheet(params) {
    // Your custom element, nested (if you want).
    const nestedCustomElement = document.querySelector('top-level-custom-element').shadowRoot.querySelector('nested-custom-element');

    // Get styles from the imported sheet, extracts their CSS as text.
    const rules = document.getElementById('styles-file-for-custom-element').sheet.cssRules;
    let rulesCSSText = '';

    // Transform into a CSS text repesentation.
    for (let index = 0; index < rules.length; index++) {
        rulesCSSText += `${rules[index].cssText} `;
    }

    // Create the new stylesheet, add rules, add the former to the web component
    var sheet = new CSSStyleSheet();
    sheet.replaceSync(rulesCSSText);
    apiRequest.shadowRoot.adoptedStyleSheets.push(sheet);
}

As per @supersharp answer here, this would work only for web components with the shadow DOM set to open mode.

Diminish answered 9/8, 2023 at 7:58 Comment(1)
Good trick to avoid duplication of codeAnaesthesiology
H
0

I was able to accomplish this by loading an external CSS file into the shadow root.

let wrapper = document.getElementsByTagName('main')[0];
let shadow = wrapper.attachShadow({ mode: "open" });

let stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.type = 'text/css';
stylesheet.href = '/css/example.css';
shadow.appendChild(stylesheet);

Works on Firefox 123, Safari 17, Chrome 121

Haematocele answered 2/3 at 18:2 Comment(0)
G
0

In Angualar use js path or query the element from document.querySelector , then add the style as shown below and keep that in ngAfterViewInit

ngAfterViewInit(): void {
   document.querySelector('#my-select').shadowRoot.querySelector('.select-icon-inner').setAttribute('style', 'opacity:1');
}
Gotthard answered 26/3 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.