I have a web component with a shadow DOM and a default slot.
I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.
I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.
Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).
const styleStr = `
:host {
display: block;
border: 3px dotted red;
}
:host(:has(div)) {
border-color: green;
}
`;
let css;
try {
css = new CSSStyleSheet;
css.replaceSync(styleStr);
} catch(e) { console.error(e) }
customElements.define('x-y', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
this.shadowRoot.append(document.createElement('slot'))
}
})
<x-y>no div - should have red border</x-y>
<x-y>
<div>div, should have green border</div>
</x-y>
I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.
Does anyone have definitive knowledge/reference about this, and can point me to some documentation?
You want to style slotted content based on an element inside the slot
Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.
If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV
Then creates a <style> element in the container element
Disclaimer: Provided code is a Proof of Concept, not production ready
<my-component>
Hello Web Component
</my-component>
<!-- <my-component> will add a STYLE element here -->
<my-component>
<!-- <my-component> will assign a unique ID to the DIV -->
<div>Web Component with a DIV in the slot</div>
</my-component>
<script>
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
let slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (evt) => {
[...slot.assignedNodes()].forEach(el => {
if (el.nodeName == "DIV") {
el.id = "unique" + new Date() / 1;
// inject a <style> before! <my-component>
this.before( Object.assign( document.createElement("STYLE"), {
innerHTML : `#${el.id} { background:lightgreen } `
}));
}
});
});
}
})
</script>
PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...
I know you can get the :root or html custom properties using window.getComputedStyle(document.body).getPropertyValue('--foo'), but I was wondering how you would get the value from a class scoped property?
For example:
body {
--background: white;
}
.sidebar {
--background: gray;
}
.module {
background: var(--background);
}
How would I get getPropertyValue('--background') from .sidebar, which would return me gray instead of white? Am I going the wrong direction by wanting to do this (I have a library that needs the colors passed to it through JS, and they are already defined as custom properties)?
Research:
I could probably query for an element with .sidebar and get it that way, but it does not seem reliable in case no such element exists.
Seems like I can list all properties (https://css-tricks.com/how-to-get-all-custom-properties-on-a-page-in-javascript/), but this process seems cumbersome.
const sideBarNodeRef = document.querySelector(".sidebar");
const sideBarBgColor = sideBarNodeRef ? sideBarNodeRef.style.backgroundColor:null
or
const sideBarBgColor = sideBarNodeRef ? sideBarNodeRef.getPropertyValue('background-color'):null
You can do it like your "Research 1.", just check if the element exists to avoid errors.
Notice that if a variable is not defined, it will inherit from the parent.
function getCssVar(selector, style) {
var element = document.querySelector(selector)
// Exit if element doesn't exist
if(!element) return false
// Exit if variable is not defined
if(!getComputedStyle(element).getPropertyValue(style)) return false
console.log(`${selector} : ${getComputedStyle(element).getPropertyValue(style)}`)
}
getCssVar('.exist-and-has-variable', '--background')
getCssVar('.exist-and-no-variable', '--background')
getCssVar('.child-with-variable', '--background')
getCssVar('.child-without-variable', '--background')
getCssVar('.dont-exist', '--background')
:root {
--background: white;
}
.exist-and-has-variable {
--background: gray;
}
.child-with-variable {
--background: red;
}
<div class="exist-and-has-variable">
<div class="child-with-variable"></div>
<div class="child-without-variable"></div>
</div>
<div class="exist-and-no-variable">
</div>
I have a div with contenteditable=true and bind:textContent={value} so it behaves pretty much like a textarea.
The only issue I have with it is that I want to override the content of the div by processing the value, but seems like it is not possible.
To test I wrote this
<div contenteditable="true" bind:textContent={value}>testVal</div>
where value is an exported property of the component.
I kind of expected value to be set to testVal, but instead the div contains the value property.
I sort of understand why this is happening and that what I am doing is sort of an edge case, but is it at all possible to change this behaviour to kind of get a one way binding to value?
and I have tried my "normal" way of creating a one way binding (with some hacks to demonstrate issues):
<div contenteditable="true" on:input={e => value = e.target.textContent}>
{#each (value || "").split("") as part}
{part}
{/each}
</div>
this looks fine, but whenever I change type in the div my input gets multiplied, i.e. if I type e the div gets updated with ee. If I add another e I get eeee
I think the way to go is to use your "normal" way of creating a one way binding. Otherwise, using multiple ways of binding on the same element will conflict.
I used a combination of on:input like you described and, inside of the div, {#html html}
The following example formats each other word in bold as you type (there's some glitch when starting with an empty field):
<script>
import {tick} from "svelte";
let html = '<p>Write some text!</p>';
// for the implementation of the two functions below, see
// https://stackoverflow.com/a/13950376/4262276
let saveSelection = (containerEl) => { /**/ };
let restoreSelection = (containerEl, savedSel) => { /**/ };
let editor;
function handleInput(e){
const savedSelection = saveSelection(editor);
html = e.target.textContent
.split(" ")
.map((t, i) => i % 2 === 0
? `<span style="font-weight:bold">${t}</span>`
: t
)
.join(" ");
tick().then(() => {
restoreSelection(editor, savedSelection);
})
}
</script>
<div
bind:this={editor}
contenteditable="true"
on:input={handleInput}
>{#html html}</div>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
I have a problem with adding javascript to handle event for a custom element . I defined a custom element in a javascript file called menu.js, by adding this element to DOM directly, like the code below:
customElements.define("custom-menu", class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<header class="header">
<div class="header__menu">
<div class="header__menu_bar"></div>
<div class="header__menu_bar"></div>
<div class="header__menu_bar"></div>
</div>
</header>
<div class="modal2">
// some code HTML
</div>
<div class="modal__details">
// some code HTML
</div>`;
}
});
const header__menu = document.querySelector(".header__menu");
header__menu.addEventListener("click", function () {
document.getElementsByClassName("modal2")[0].style.width = "100%";
$("body").addClass("stop-scrolling");
$("body").removeClass("enable-scrolling");
if (document.body.offsetWidth <= "640") {
document.getElementsByClassName("modal-content")[0].style.width = "100%";
} else {
document.getElementsByClassName("modal-content")[0].style.width = "290px";
}
document.getElementsByClassName("modal-content")[0].style.right = "0";
});
// some code Javascript handle class "modal2" and "modal__details"
I use connectedCallback() function to call everytime custom element is inserted into the DOM. If I add Javascipt code directly like the code above, I have succeeded to add click event to the div with class "header__menu" and handle both the modals. Now I want to put the Javascipt code after customElements.defined(...); to another Javascipt file and link this file to file HTML using this element to do the same task but it doesn't work as when I add directly. Can someone tell me the reason?
Thank you!
You have to make sure the js code get called after the custom element is created.
For example:
$(function () {
const header__menu = document.querySelector(".header__menu");
header__menu.addEventListener("click", function () {
document.getElementsByClassName("modal2")[0].style.width = "100%";
$("body").addClass("stop-scrolling");
$("body").removeClass("enable-scrolling");
if (document.body.offsetWidth <= "640") {
document.getElementsByClassName("modal-content")[0].style.width = "100%";
} else {
document.getElementsByClassName("modal-content")[0].style.width = "290px";
}
document.getElementsByClassName("modal-content")[0].style.right = "0";
});
});
When I style the custom element, I put css code to another file then link to HTML file like I usually work with HTML tag, class or id. This work well, and when I inspect by F12, I can see all html codes of custom element and styles of them.
For example, this is style of the div with class name modal__details:
.modal__details {
position: fixed;
top: 0;
right: 0;
z-index: 2;
width: 0;
height: 100%;
transition: 0.4s;
background-color: black;
}
I don't need to specify anything. It works like any other div. It means my custom element is in HTML DOM, but I just can handle it by put Javascript directly like above, it doesn't work in other JS file. I have also tried the way you instruct, but it has failed. So it's my wondering about that.
Alright, I'm creating a system for my webpage that allows users to change the theme. How I want to accomplish this is by having all the colors as variables, and the colors are set in the :root part of the CSS.
What I want to do is change those colors via JavaScript. I looked up how to do it, but nothing that I attempted actually worked properly. Here's my current code:
CSS:
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
JS:
(Code to set the theme, it's ran on the click of a button) - I didn't bother adding the :root change to the other 2 themes since it doesn't work on the Dark theme
function setTheme(theme) {
if (theme == 'Dark') {
localStorage.setItem('panelTheme', theme);
$('#current-theme').text(theme);
$(':root').css('--main-color', '#000000');
}
if (theme == 'Blue') {
localStorage.setItem('panelTheme', 'Blue');
$('#current-theme').text('Blue');
alert("Blue");
}
if (theme == 'Green') {
localStorage.setItem('panelTheme', 'Green');
$('#current-theme').text('Green');
alert("Green");
}
}
(Code that is ran when the html is loaded)
function loadTheme() {
//Add this to body onload, gets the current theme. If panelTheme is empty, defaults to blue.
if (localStorage.getItem('panelTheme') == '') {
setTheme('Blue');
} else {
setTheme(localStorage.getItem('panelTheme'));
$('#current-theme').text(localStorage.getItem('panelTheme'));
}
}
It shows the alert, but does not actually change anything. Can someone point me in the right direction?
Thank you #pvg for providing the link. I had to stare at it for a little to understand what was going on, but I finally figured it out.
The magical line I was looking for was this:
document.documentElement.style.setProperty('--your-variable', '#YOURCOLOR');
That did exactly what I wanted it to do, thank you very much!
For those who want to modify the actual style sheet the following works:
var sheet = document.styleSheets[0];
sheet.insertRule(":root{--blue:#4444FF}");
More info at here: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
I think this is cleaner and easier to remember.
to set/get css variables to/from :root
const root = document.querySelector(':root');
// set css variable
root.style.setProperty('--my-color', 'blue');
// to get css variable from :root
const color = getComputedStyle(root).getPropertyValue('--my-color'); // blue
Example: setting multiple variables all at once
const setVariables = vars => Object.entries(vars).forEach(v => root.style.setProperty(v[0], v[1]));
const myVariables = {
'--color-primary-50': '#eff6ff',
'--color-primary-100': '#dbeafe',
'--color-primary-200': '#bfdbfe',
'--color-primary-300': '#93c5fd',
'--color-primary-400': '#60a5fa',
'--color-primary-500': '#3b82f6',
'--color-primary-600': '#2563eb',
'--color-primary-700': '#1d4ed8',
'--color-primary-800': '#1e40af',
'--color-primary-900': '#1e3a8a',
};
setVariables(myVariables);
To use the values of custom properties in JavaScript, it is just like standard properties.
// get variable from inline style
element.style.getPropertyValue("--my-variable");
// get variable from wherever
getComputedStyle(element).getPropertyValue("--my-variable");
// set variable on inline style
element.style.setProperty("--my-variable", 4);
I came here looking how to toggle the :root color-scheme with JavaScript, which sets the browser to dark mode (including the scroll bars) like this:
:root {
color-scheme: dark;
}
using the #Daedalus answer above, this is how I implemented my dark mode detection from user preference:
const userPrefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const preferredTheme = userPrefersDarkMode ? 'dark' : 'light';
document.documentElement.style.setProperty("color-scheme", preferredTheme);
or with saved toggle:
const savedTheme = localStorage.getItem('theme');
if (savedTheme == 'dark') {
thisTheme = 'light'
}
else {
thisTheme = 'dark'; // the default when never saved is dark
}
document.documentElement.style.setProperty("color-scheme", thisTheme);
localStorage.setItem('theme', thisTheme);
see also the optional meta tag in the header:
<meta name="color-scheme" content="dark light">
old jquery magic still working too
$('#yourStyleTagId').html(':root {' +
'--your-var: #COLOR;' +
'}');
TL;DR
A solution to the problem could be the below code:
const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");
styleTag.innerHTML = `
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
`;
headTag.appendChild(styleTag);
Explanation:
Although #Daedalus answer with document.documentElement does the job pretty well, a slightly better approach is to add the styling into a <style> HTML tag (solution proposed).
If you add document.documentElement.style then all the CSS variables are added into the html tag and they are not hidden:
On the other hand, with the proposed code:
const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");
styleTag.innerHTML = `
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
`;
headTag.appendChild(styleTag);
the HTML tag will be cleaner and if you inspect the HTML tag you can also see the :root styling as well.
Read only, retrieve all CSS --root rules in an array, without using .getComputedStyle().
This may allow to retrieve values before full DOM content load, to create modules that use global root theme variables, but not via CSS. (canvas context...)
/* Retrieve all --root CSS variables
* rules into an array
* Without using getComputedStyle (read string only)
* On this example only the first style-sheet
* of the document is parsed
*/
console.log(
[...document.styleSheets[0].rules]
.map(a => a.cssText.split(" ")[0] === ":root" ?
a.cssText.split("{")[1].split("}")[0].split("--") : null)
.filter(a => a !== null)[0]
.map(a => "--"+a)
.slice(1)
)
:root {
--gold: hsl(48,100%,50%);
--gold-lighter: hsl(48,22%,30%);
--gold-darker: hsl(45,100%,47%);
--silver: hsl(210,6%,72%);
--silver-lighter: hsl(0,0%,26%);
--silver-darker: hsl(210,3%,61%);
--bronze: hsl(28,38%,67%);
--bronze-lighter: hsl(28,13%,27%);
--bronze-darker: hsl(28,31%,52%);
}
/*My style*/
:root {
--hl-color-green: green;
}
/*My update*/
* {
--hl-color-green: #0cc120;
}