I've been trying to make a custom HTML Element by extending the HTMLElement class. I try adding some style to it by linking a CSS file that is in the same directory as my other two files - index.html and custom.css.
Main folder
index.html
custom.css
custom.js
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="nofollow" type="text/css" href=''>
</head>
<body>
<script src="./custom.js"></script>
<smooth-button text="Smooth button" no-1 = 1 no-2 = 2></smooth-button>
</body>
</html>
custom.css:
smooth-button{
display: block;
color: blue;
background-color: orange;
}
custom.js:
class SmoothButton extends HTMLElement{
constructor(){
super();
this.shadow = this.attachShadow({mode: "open"})
}
connectedCallback(){
this.render();
}
render(){
this.SumOfNo1AndNo2 = null;
if(this.getAttribute("no-1")!=null && this.getAttribute("no-2")!=null){
this.SumOfNo1AndNo2 = parseInt(this.getAttribute("no-1")) +
parseInt(this.getAttribute("no-2"));
}
else{
console.log("Invalid attribute.")
}
this.shadow.innerHTML = `<button>` + this.getAttribute("text") + " " + this.SumOfNo1AndNo2
+ "</button>"
}
}
customElements.define("smooth-button", SmoothButton);
With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of. How can I apply the styles separately to each of its elements (just a <button> for now) with an external CSS file? I'm using external CSS because it's somehow better as I read it here.
In addition to the answers from Brad and Emiel,
(Brad) Bluntly add a <style> element inside shadowDOM
Do read about adopted StylesSheets (Chromium only)
(Emiel) use cascading CSS properties
There are more options to style shadowDOM:
Learn about Inheritable Styles
https://lamplightdev.com/blog/2019/03/26/why-is-my-web-component-inheriting-styles/
use shadow parts
<style>
::part(smoothButton){
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button></smooth-button>
<smooth-button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
constructor(){
super()
.attachShadow({mode:"open"})
.innerHTML = `<button part="smoothButton">LABEL</button>`;
}
});
</script>
https://developer.mozilla.org/en-US/docs/Web/CSS/::part
https://meowni.ca/posts/part-theme-explainer/
https://css-tricks.com/styling-in-the-shadow-dom-with-css-shadow-parts/
https://dev.to/webpadawan/css-shadow-parts-are-coming-mi5
https://caniuse.com/mdn-html_global_attributes_exportparts
But...
The first question you should ask yourself:
Do I really need shadowDOM?
If you don't want its encapsulating behavior, then do not use shadowDOM
<style>
.smoothButton{
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button></smooth-button>
<smooth-button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
connectedCallback(){
this.innerHTML = `<button class="smoothButton">LABEL</button>`;
}
});
</script>
shadowDOM <slot>
Another alternative is to use shadowDOM <slot> elements, because they are styled by its container element
<style>
.smoothButton{
display: block;
color: blue;
background-color: orange;
}
</style>
<smooth-button><button class="smoothButton">LABEL</button></smooth-button>
<smooth-button><button class="smoothButton">LABEL</button></smooth-button>
<script>
customElements.define("smooth-button", class extends HTMLElement {
constructor(){
super()
.attachShadow({mode:"open"})
.innerHTML = `<slot></slot>`;
}
});
</script>
When you go down the <slot> rabbithole, be sure to read the (very long) post:
::slotted CSS selector for nested children in shadowDOM slot
Additionally to Brad's answer. One of the ways you can apply styles from the Light DOM to the Shadow DOM is with CSS Variables.
smooth-button{
display: block;
--button-color: blue;
--button-background-color: orange;
}
render() {
this.shadow.innerHTML = `
<style>
button {
color: var(--button-color);
background-color: var(--button-background-color);
}
</style>
<button>
${this.getAttribute("text")} ${this.SumOfNo1AndNo2}
</button>
`;
)
With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of.
This is actually how the custom element is supposed to work. You can't apply styles to the shadow DOM from the outer document. If you could, you'd have a high likelihood of breaking the custom element styling through external modification.
All is not lost however! The reason the button is a different color from its background is due to the user agent stylesheet. You can actually set some CSS to tell the background to inherit the parent background color. Try adding this to your custom element:
const style = document.createElement('style');
style.textContent = `
button {
background: inherit;
}
`;
this.shadow.append(style);
JSFiddle: https://jsfiddle.net/5t2m3bku/
(Also note that it's not really a great idea to interpolate/concatenate text directly into HTML. That text gets interpreted as HTML, which can lead to invalid HTML if reserved characters are used, and even potential XSS vulnerabilities. You might modify that line where you set innerHTML to set the text, or switch to a template engine.)
Related
New to using custom HTML elements. My class style is not applying to the page render, though it shows in the DOM. The only clue is that it appears in the inspector within the open Shadow DOM rather than the regular DOM, which seems undesirable if it causes CSS issues.
Note the class style in question in this example is called 'border-all'. I have tried three modern browsers. No border displays
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Test</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" >
<script src="header.js"></script>
</head>
<body>
<header-component></header-component>
</body>
</html>
header.js
class Header extends HTMLElement {
constructor() {
super();
const template = document.createElement('template');
const h1 = document.createElement('h1')
h1.innerHTML = 'Hello World'
h1.style.color = 'green'
h1.className = 'border-all'
template.content.appendChild(h1)
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.content);
}
}
customElements.define('header-component', Header);
https://developers.google.com/web/fundamentals/web-components/shadowdom
ShadowDOM is styled by:
<style> within shadowDOM
Inheritable styles
https://lamplightdev.com/blog/2019/03/26/why-is-my-web-component-inheriting-styles/
(cascading) CSS properties
shadowParts (and Themes)
https://meowni.ca/posts/part-theme-explainer/
<slot> are reflected, they are NOT styled by the shadowDOM, but by its container.
See: ::slotted content
(feb 2022) Constructible StyleSheets is still a Chromium only party
https://caniuse.com/mdn-api_cssstylesheet_cssstylesheet
customElements.define("my-component",class extends HTMLElement{
constructor(){
super().attachShadow({mode:"open"})
.innerHTML = `
<style>
:host { display:inline-block; padding-left:2em }
h2 { margin:0 }
span {
color: var(--spancolor,grey);
</style>
<h2 part="wcTitle">Hello Web Component!</h2>
<span>styling shadowDOM can be a challenge</span>
<slot></slot>`;
}
})
<style>
body {
font:18px Arial; /* inheritable styles style Web Components */
color:green; /* color is an inheritable style */
--spancolor: lightcoral; /* css properties cascade, and are available in Web Components */
}
div ::part(wcTitle){ /* ::parts style ALL (nested) elements in shadowDOM */
background:gold;
}
my-component{
font-weight: bold; /* slotted content is styled by the container */
}
my-component my-component{
color:blue;
}
</style>
<my-component>But is very powerfull!</my-component>
<div>
<my-component>You should never give up!
<my-component>Those who quit after the first attempt are loosers</my-component>
</my-component>
</div>
One way of doing this is to use the Element.classList property
You can just:
document.querySelector('h1').classList.add('border-all');
I will leave you below a working example in CodePen, the only difference is that the element already have and id and we use
document.getElementById
instead of
document.querySelector
But thats pretty much the difference, I hope this helps you understand better!
https://codepen.io/Jeysoon/pen/NWwzEJG?editors=1111
I have created a webcomponent for a generic input boxes that i needed across multiple projects.
the design functionality remains same only i have to use switch themes on each projects.so i have decided to go on with webcomponents.One of the projects is based on Vue Js.In Vue js the DOM content is re-rendered while each update for enabling reactivity. That re-rendering of vue template is reinitializing my custom webcomponent which will result in loosing all my configurations i have assigned to the component using setters.
I know the below solutions. but i wanted to use a setter method.
pass data as Attributes
Event based passing of configurations.
Using Vue-directives.
using v-show instead of v-if
-- Above three solutions doesn't really match with what i am trying to create.
I have created a sample project in jsfiddle to display my issue.
Each time i an unchecking and checking the checkbox new instances of my component is creating. which causes loosing the theme i have selected. (please check he active boxes count)
For this particular example i want blue theme to be displayed. but it keep changing to red
JSFiddle direct Link
class InputBox extends HTMLElement {
constructor() {
super();
window.activeBoxes ? window.activeBoxes++ : window.activeBoxes = 1;
var shadow = this.attachShadow({
mode: 'open'
});
var template = `
<style>
.blue#iElem {
background: #00f !important;
color: #fff !important;
}
.green#iElem {
background: #0f0 !important;
color: #f00 !important;
}
#iElem {
background: #f00;
padding: 13px;
border-radius: 10px;
color: yellow;
border: 0;
outline: 0;
box-shadow: 0px 0px 14px -3px #000;
}
</style>
<input id="iElem" autocomplete="off" autocorrect="off" spellcheck="false" type="text" />
`;
shadow.innerHTML = template;
this._theme = 'red';
this.changeTheme = function(){
this.shadowRoot.querySelector('#iElem').className = '';
this.shadowRoot.querySelector('#iElem').classList.add(this._theme);
}
}
connectedCallback() {
this.changeTheme();
}
set theme(val){
this._theme = val;
this.changeTheme();
}
}
window.customElements.define('search-bar', InputBox);
<!DOCTYPE html>
<html>
<head>
<title>Wrapper Component</title>
<script src="https://unpkg.com/vue"></script>
<style>
html,
body {
font: 13px/18px sans-serif;
}
select {
min-width: 300px;
}
search-bar {
top: 100px;
position: absolute;
left: 300px;
}
input {
min-width: 20px;
padding: 25px;
top: 100px;
position: absolute;
}
</style>
</head>
<body>
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
</div>
</script>
<script type="text/x-template" id="select2-template">
<select>
<slot></slot>
</select>
</script>
<script>
var vm = new Vue({
el: "#el",
template: "#demo-template",
data: {
visible: true,
},
mounted(){
let self = this
setTimeout(()=>{
self.$refs.iBox.theme = 'blue';
} , 0)
}
});
</script>
</body>
</html>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
Vue's v-if will add/remove the whole DIV from the DOM
So <search-bar> is also added/removed on every checkbox click
If you want a state for <search-bar> you have to save it someplace outside the <search-bar> component:
JavaScript variable
localStorage
.getRootnode().host
CSS Properties I would go with this one, as they trickle into shadowDOM
...
...
Or change your checkbox code to not use v-if but hide the <div> with any CSS:
display: none
visibility: hidden
opacity: 0
move to off screen location
height: 0
...
and/or...
Managing multiple screen elements with Stylesheets
You can easily toggle styling using <style> elements:
<style id="SearchBox" onload="this.disabled=true">
... lots of CSS
... even more CSS
... and more CSS
</style>
The onload event makes sure the <style> is not applied on page load.
activate all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = false
remove all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = true
You do need CSS Properties for this to work in combo with shadowDOM Elements.
I prefer native over Frameworks. <style v-if='visible'/> will work.. by brutally removing/adding the stylesheet.
I am using lit html to create custom web components in my project. And my problem is when I try to use the CSS target selector in a web component it wont get triggered, but when I am doing it without custom component the code works perfectly. Could someone shed some light to why this is happening and to what would be the workaround for this problem? Here is my code:
target-test-element.js:
import { LitElement, html} from '#polymer/lit-element';
class TargetTest extends LitElement {
render(){
return html`
<link rel="stylesheet" href="target-test-element.css">
<div class="target-test" id="target-test">
<p>Hello from test</p>
</div>
`;
}
}
customElements.define('target-test-element', TargetTest);
with the following style:
target-test-element.css:
.target-test{
background: yellow;
}
.target-test:target {
background: blue;
}
and I created a link in the index.html:
index.html(with custom component):
<!DOCTYPE html>
<head>
...
</head>
<body>
<target-test-element></target-test-element>
Link
</body>
</html>
And here is the working one:
index.html(without custom component)
<!DOCTYPE html>
<head>
...
</head>
<body>
Link
<div class="target-test" id="target-test">
Hello
</div>
</body>
</html>
LitElement uses a Shadow DOM to render its content.
Shadow DOM isolates the CSS style defined inside and prevent selecting inner content from the outide with CSS selectors.
For that reason, the :target pseudo-class won't work.
Instead, you could use a standard (vanilla) custom element instead of the LitElement.
With no Shadow DOM:
class TargetTest extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div>
<span class="test" id="target-test">Hello from test</span>
</div>`
}
}
customElements.define('target-test-element', TargetTest)
.test { background: yellow }
.test:target { background: blue }
<target-test-element></target-test-element>
Link
Alternately, if you still want to use a Shadow DOM, you should then set the id property to the custom element itself. That supposes there's only one target in the custom element.
class TargetTest extends HTMLElement {
connectedCallback() {
this.attachShadow( { mode: 'open' } ).innerHTML = `
<style>
:host( :target ) .test { background-color: lightgreen }
</style>
<div>
<span class="test">Hello from test</span>
</div>`
}
}
customElements.define('target-test-element', TargetTest)
<target-test-element id="target-test"></target-test-element>
Link
Bit in late, i've experienced the same problem! So i'm following one of two paths:
Use a lit element but without the shadowDOM, to do that in your Lit element call the method createRenderRoot()
createRenderRoot () {
return this
}
Instead handle the CSS logic with :target i'm handling the attribute on the element (easy to do with Lit) eg. active and use it in CSS:
element[active] {
/* CSS rules */
}
These are my solutions for the moment! Hope it's help...
Is it possible to set two Values for a single attribute on a CSS ID, e.g.
height: 12em; height: 12rem;
for a single id?
I know it's possible to do this with CSS, but I want to change the height via javascript, and have both values on one attribute, so that 'em' functions as a fallback for Browsers not supporting 'rem'.
Example:
function updateHeight() {
var testElem = document.getElementById("testId");
testElem.style.height = "12em";
testElem.style.height = "12rem";
//testElem.style.height = "12em; 12rem;"; doesn't work, applies "12em"
}
#testId {
height: 8em;
height: 8rem;
background-color: black;
color: white;
}
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<meta charset="utf-8" />
</head>
<body>
<div id="testId">
BlaBla
</div>
<button onclick="updateHeight()">Bla</button>
</body>
</html>
You can use a class instead
function updateHeight() {
var testElem = document.getElementById("testId");
testElem.classList.add('high');
}
#testId {
height: 8em;
height: 8rem;
background-color: black;
color: white;
}
#testId.high { /* remember that ID is more specific than class */
height: 12em;
height: 12rem;
}
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<meta charset="utf-8" />
</head>
<body>
<div id="testId">
BlaBla
</div>
<button onclick="updateHeight()">Bla</button>
</body>
</html>
Or if the value is dynamic, you'd probably have to insert a style tag to be able to support CSS cascading
function updateHeight() {
var value = 12; // or something dynamic
var styles = '#testId {height: '+value+'em; height: '+value+'rem;}';
var tag = document.createElement('style');
tag.innerHTML = styles;
document.querySelector('head').appendChild(tag);
}
#testId {
height: 8em;
height: 8rem;
background-color: black;
color: white;
}
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<meta charset="utf-8" />
</head>
<body>
<div id="testId">
BlaBla
</div>
<button onclick="updateHeight()">Bla</button>
</body>
</html>
Yes, you can. This is defined by CSS Cascade:
Each property declaration applied to an element contributes a
declared value for that property associated with the
element.
These values are then processed by the cascade to choose a single
“winning value”.
The cascaded value represents the result of the
cascade: it is the declared value that wins the cascade (is
sorted first in the output of the cascade).
Then, if you use
height: 8em;
height: 8rem;
there will be 2 declared values for the property height: 8em and 8rem. The latter will will the cascade, becoming the cascaded value.
If some browser does not support one of them, the other one will be the only declared value, so it will win the cascade.
If some browser does not support any of them, the output of the cascade will be the empty list, and there won't be any cascaded value. The specified value will be the result of the defaulting processes (the initial value in this case, because height is not an inherited property).
However, with JavaScript there is a problem. setProperty, used under the hood by camel case IDL properties in .style, uses the set a CSS declaration algorithm, which overwrites existing declarations:
If property is a case-sensitive match for a property name of a CSS declaration in declarations, let declaration be that CSS
declaration.
Otherwise, append a new CSS declaration with the property name property to declarations and let declaration be that CSS declaration.
There are some workarounds:
Use cssText instead of camel case IDL attributes:
testElem.style.cssText += "; height: 12em; height: 12rem; ";
Check the style after setting it. If you get the empty string, it means it wasn't recognized.
testElem.style.height = "12rem";
if(!testElem.style.height) testElem.style.height = "12em";
Don't set the styles in the inline style declaration. Create a new stylesheet instead, or add some class to the element to trigger some CSS from an existing stylesheet.
How can I append style element to DOM without eliminating existing style on the item (eg color, text-align, etc)?
The event calls the function, but the problem is 'Style' gets completely replaced with the single item instead.
I have simple code triggered on the event:
function changeback(onoff) {
if(onoff) {
document.getElementById("field1").style.background="#fff";
} else
document.getElementById("field1").style.background="#000";
}
Here is how you can do it :
var elem = document.getElementById('YOUR ELEMENT ID');
elem.style.setProperty('border','1px solid black','');
elem.style is an object implementing the CSSStyleDeclaration interface which supports a setProperty function. If you check the style property in Firebug now, you will notice addition of the border property within the style attribute of the element.
Which browser are you using? In Chrome, this works for me:
<html>
<head>
<style type="text/css">
.test { background: #ff0000; font-family: "Verdana"; }
</style>
<script type="text/javascript">
function changeback(onoff)
{
if(onoff){
document.getElementById("field1").style.background="#0f0";
} else {
document.getElementById("field1").style.background="#000";
}
}
</script>
</head>
<body>
<h1>Test</h1>
<p id="field1" onclick="changeback(true);" class="test">This is a test</p>
</body>
</html>
When I click on the text, the background color changes, but the rest of the style (in this case, the font) stays the same.
Is that what you're trying to do?