I am building a widget for third-party websites, using shadow DOM to prevent their CSS from interfering with ours. I am using the ShadyDOM and ShadyCSS polyfills to make it work in Edge and IE, but it is not transforming the CSS for the shadow DOM as I would expect.
Example:
<!DOCTYPE html>
<html>
<head>
<title>Shadow DOM test</title>
</head>
<body>
<div id="container">container is here</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.3.0/webcomponents-bundle.js"></script>
<script>
const shadow = document.getElementById("container").attachShadow({ mode: "open" });
const style = document.createElement("style");
style.innerHTML = `
:host .stuff {
background: #ff00ff;
}
`;
shadow.appendChild(style);
const div = document.createElement("div");
div.classList.add("stuff");
div.innerHTML = "stuff inside shadow dom";
shadow.appendChild(div);
</script>
</body>
</html>
In Chrome (which supports shadow DOM natively), the stuff div has a pink background, as I would expect. But in Edge (which does not support shadow DOM natively), I see the "stuff inside shadow dom" text (meaning my script ran and the ShadyDOM functions worked), but I don't see the pink background.
Why is this happening? I am attaching a shadow root to a plain old div, instead of using custom elements as the example in the ShadyCSS README does, but does that matter? If so, how can I make this work? I am working on a big, existing app, and not wanting to make too many changes at once, so I would strongly prefer to use the standard HTML elements I am already using (divs, buttons, etc.) instead of coming up with my own elements or templates, although I would be willing to consider templates and/or custom elements if it can be done easily, without having to make a lot of big changes.
With ShadyCSS
:host CSS pseudo-element is not known in Edge.
To make it work, you should use ShadyCSS.prepareTemplate() that will replace :host by the name of the custom element and define the style as a global style that will apply to all the page.
Remember that there's no Shadow DOM in Edge: there's no boundaries/scope for CSS with a fake/polyfilled Shadow DOM.
In your case you could use ShadyCSS.prepareTemplate( yourTemplate, 'div' ) as in the example below:
ShadyCSS.prepareTemplate( tpl, 'div' )
container.attachShadow( { mode: "open" } )
.appendChild( tpl.content.cloneNode(true) )
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.3.0/webcomponents-bundle.js"></script>
<template id=tpl>
<style>
:host .stuff {
background: #ff00ff;
}
</style>
<div class=stuff>stuff inside shadow dom</div>
</template>
<div id=container>container is here</div>
Note: since the polyfill will replace :host by div and add it as a global style, you could observe some side effects if you have another HTML code part that matches div .stuff.
Without ShadyCSS
ShadyCSS was designed for Custom Elements, but not really for standard elements. However, you should get inspiration from the polyfill and create explicitely the styles properties for fake (polyfilled) Shadow DOM. In your case replace :host with div#containter:
container.attachShadow( { mode: "open" } )
.appendChild( tpl.content.cloneNode(true) )
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.3.0/webcomponents-bundle.js"></script>
<template id=tpl>
<style>
div#container .stuff {
background: #ff00ff;
}
:host .stuff {
background: #ff00ff;
}
</style>
<div class=stuff>stuff inside shadow dom</div>
</template>
<div id=container>container is here</div>
Related
I am working on a web extension that adds a custom overlay element to any website. naturally I don't want the websites CSS and JavaScript to interfere with my custom element, nor do I want to influence the appearance of any websites in any way other than with my overlay.
So naturally I turn to Shadow DOM. I soon learn that I can't simply attach my shadow DOM to document.body since this will displace the whole page.
It turns out I need one regular element in the body to act as a shadow root:
const shadowHost = document.createElement('div');
document.body.appendChild(shadowHost);
const shadow = shadowHost.attachShadow({mode: 'closed'});
But how can I protect this shadow root from being influenced by the css or js of the website?
What have you tried?
shadowRoots are not styled by global CSS, unless they are inheritable styles
https://lamplightdev.com/blog/2019/03/26/why-is-my-web-component-inheriting-styles/
mode:"closed" has nothing to do with CSS, it just doesn't publish the shadowRoot in element.shadowRoot, making it impossible to access the shadow DOM from the outside.
When the mode of a shadow root is "closed", the shadow root’s implementation internals are inaccessible and unchangeable from JavaScript—in the same way the implementation internals of, for example, the <video> element are inaccessible and unchangeable from JavaScript.
document.body
.appendChild(document.createElement('div'))
.attachShadow({mode:'closed'})
.innerHTML = `<style>h1{color:green}</style><h1>Hello</h1><h2>Component</h2>`;
<style>
body {
/* inheritable styles do style shadowRoots */
font: 10px Arial;
color: red;
}
</style>
Im injecting some Javascript that creates an isolated div located at the top of the body. Within this div there is a shadowDom element. The reason I went with shadowDom is because I thought it stoped CSS from bleeding in to all the divs within the shadowDom. But I can clearly see that it is inheriting style from the tag(font-size: 62.5%;). This is causing my text to be smaller. I can override this with adding font-size: 100% !Important but even though it crosses it out in the inspector tools it does not actually change. The only way I can get it to work is by unchecking the box in the CSS portion.
Please Help
Thanks,
Dev Joe
HTML Shadow Dom IMAGE
CSS Checked IMAGE
CSS Unchecked IMAGE
You should not use a relative font size (like 100%) because it applies to inherited size... so this will have no effect.
Insead, you should define a rule to the :host CSS peudo-class:
:host {
font-size: initial ;
}
NB: You'll need to add !important only if the font-size defined in the container (the main document) applies to the host element directly.
NB #2: You can use all: initial instead but you cannot combine it with !important.
host.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
:host { all: initial }
</style>
Inside Shadow Root <br>
<div>Div in Shadow DOM</div>
<slot></slot>
`
body { font-size : 62.5% ; color: red }
Small Font
<div>Div in main Document</div>
<div id=host>Light DOM</div>
No need for shadow dom, just use the all attribute to disable the inheritance.
#myElement {
all: initial;
}
I need a selector for usage in css inside a shadow root, which selects all children (but not grand children) of the shadow root, no matter what tag they are and without giving them an ID.
In the example below, SPAN,A,P and DIV should get a red border, but SPAN IN DIV not.
<my-element>
#shadow-root
<span>SPAN</span>
<a>A</a>
<p>P</p>
<div>
DIV
<span>SPAN IN DIV</span>
</div>
<style>
:root>*{border:1px red solid;}
</style>
</my-element>
I hoped, the :root-Selector would do the job inside of a shadow dom, but thats not the case.
It would also be a possible solution if someone shows how to set an ID on the shadow root.
Update:
Tried using #shadow-root > * as selector:
seems not to work. Probably it is just google chrome developer tools visualizing the shadow root element like that.
Use this selector: :host > *
The :host selector is described in https://drafts.csswg.org/css-scoping/#host-selector
document.querySelector( 'my-element' )
.attachShadow( { mode: 'open' } )
.innerHTML = `
<span>SPAN</span>
<a>A</a>
<p>P</p>
<div>
DIV
<span>SPAN IN DIV</span>
</div>
<style>
:host>*{border:1px red solid;}
</style>`
<my-element>
</my-element>
:host may also hold a compound selector, which must be places in brackets. E.g. :host([foo=bar]) selects a host element which has attribute foo set to bar.
we can use the shadow() method from cypress. you can use the get('selector before the shadow-root') method then shadow() method and use the find('locator') till your control/elements and at last you invoke the actual method e.g. click() or type() or select('index') on that control/element. Also you can use {force:true} aswell.
cy.get("mc-select[name='taxTypeCodes']").shadow().find('div.mc-component-template ').find('div').find('label.mc-input__container').find('div.mc-input__field').find('select').select('IN3',{force:true})
For more details please refer-cypress-shadow-dom
I read many articles regarding shadow DOM but not clear about this. Can
anyone tell what is shadow DOM and how to add one for below code?
<html>
<head></head>
<body>
<div id="box"></div>
</body>
</html>
Shadow DOM is just an abstraction for DOM and CSS which is not included in the main document's DOM.
One example where Shadow DOM is used is in browsers. When you create an <input type="range" />
Chrome (or another Webkit-powered browser) will probably render a slider web component. While this slider is not part of the main document's DOM, the browser leverages the Shadow DOM to show this.
Shadow DOM is used heavily in cases where the presentation may differ from the code, mostly for preventing CSS leaking into the main component. You can find this pattern in many popular CSS frameworks.
In order to attach a "shadow" element you can use the Shadow DOM API, like so:
var shadow = document.querySelector('#box').attachShadow({
mode: 'open'
});
shadow.innerHTML = '<p>A box in the shadows</p>';
To add a Shadow DOM to your code, add this script at the end of your HTML file:
<script>
//create a Shadow DOM
var shadowRoot = box.attachShadow( { mode: 'open' } )
//add some content
shadowRoot.innerHTML = 'RTFM!'
</script>
The Shadow DOM is rendered instead of the initial DOM tree (called the Light DOM).
You can also insert content of the Light DOM in the Shadow DOM with the help of <slot> elements:
function add() {
box.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
::slotted( span ) {
color: blue ;
font-style: italic ;
outline: none !important ;
min-width: 0 !important;
}
</style>
Hello <slot name="Name"></slot>!`
}
#box [contenteditable] {
outline: 1px solid lightgray ;
display: inline-block ;
min-width: 50px ;
}
<div id=box>
Type your name:
<span slot="Name" contenteditable>Mickey</span>
<br><button onclick="add()">Add Shadow</button>
</div>
I'm using Polymer and I noticed that the :target css selector doesn't work.
For example
<polymer-element name="my-element" noscript>
<template>
<style>
:target {
border: 2px solid red;
}
</style>
<div id="test">This is a :target test</div>
</template>
</polymer-element>
Click me
<my-element></my-element>
DEMO
Any suggestions how I can fix this ?
I must admit: I'm not very familar with shadow DOM and absolutely not familar with Polymer but I'd like to tell you my view on this because your intention looks somewhat strange to me and this is too long for a comment.
Short
You can't use the pseudo selector :target within a shadow host.
Long
Unfortunately I was not able to find clear evidences in these resources
http://www.w3.org/TR/shadow-dom/
http://dev.w3.org/csswg/css-scoping/
but some hints...
The goal of Web Components was to give us the ability to build individual and isolated components that can be used in a document without caring of their inner function or style.
If a component could directly reach the "outside" document or if the outside document could reach any shadow hosts element directly, this would completely break the intention of Web Components.
Imagine what would happen if you insert two instances of your <my-element>. Both contain the same ID, which one should be targeted?
Of course it's possible to reach the shadow document, or the containing document from within the shadow document, but only through ::shadow or :host respectively.
To me its logical that the browser can't select elements using a mere :target selector since the target is the matter of the document (it's URL is targeted to some ID) not of any shadow DOM. It's also not possible to reach a shadow tree node with document.getElementById() from within the container document.
The CSS scoping spec which also adresses the Shadow DOM concepts states:
Why is the shadow host so weird?
The shadow host lives outside the shadow tree, and its markup is in
control of the page author, not the component author.
It would not be very good if a component used a particular class name
internally in a shadow tree, and the page author using the component
accidentally also used the the same class name and put it on the host
element. Such a situation would result in accidental styling that is
impossible for the component author to predict, and confusing for the
page author to debug.
(3.1.1. Host Elements in a Shadow Tree)
I'd say this is another evidence: the shadow host (viewed from outside) itself will keep the active (focus) state while handling the focus inside its tree.
To maintain encapsulation, the value of the Document object's focus
API property activeElement must be adjusted. To prevent loss of
information when adjusting this value, each shadow root must also have
an activeElement property to store the value of the focused element in
the shadow tree.
(6.3 Active Element)
One possible solution to your problem
If your intention was to highlight only the div, when your shadow element is :targeted this might be the correct style within your shadow document:
<polymer-element name="my-element" constructor="" attributes="">
<template>
<style>
:host(:target) #inner {
color: #0c0;
}
</style>
<content>Hello World!</content>
<div id="inner">This is a :target test</div>
...
It will highlight the <div> with green text, when your shadow element <my-element id="outer"></my-element> is targeted by #outer.
If this was not your intention and you really wanted to be able to target #inner from outside, I'd say this is not possible (see the "longer" part ;).
I don't think it's a good idea to link to elements inside shadow dom, because you may have multiple instances of the outer element in same page so you'll get multiple elements with same id.
However when you request a url with #elementId the browser will only look in light dom for the according element.
If you still need to style shadow dom elements you could simulate :target selector:
<polymer-element name="my-element" constructor="" attributes="">
<template>
<style>
#inner[target] {
border: 2px solid red;
}
</style>
<content>Hello World!</content>
<div id="inner" target?="{{innerTargetted}}">This is a :target test</div>
</template>
<script>
Polymer('my-element', {
ready: function() {
$(window).on('hashchange', function() {
this.innerTargetted = window.location.hash == '#inner';
}.bind(this));
}
});
</script>
</polymer-element>
Demo.