How to add/update scoped CSS variables via JavaScript - javascript

Is there a native API which can update the CSS variables scoped under a particular CSS class(or even any other complex CSS selector)predefined in a stylesheet? The question can be generalized for not just CSS variables but other CSS properties as well, i.e whether class specific CSS properties can be updated without targeting a specific HTML element, but by targeting the class definition itself.
Please find below the code snippets which demonstrates an example scenario. You can also find comments in the code to as to what I believe is happening/I am doing on specific lines.
var toggle = true;
function changeColor() {
document.documentElement.style.setProperty('--bg-color', toggle ? 'green' : 'red');
// this works for the "outer" div since there we receive global value(value defined in :root) of --bg-color
toggle = !toggle;
// here I want to also change the scoped value of --bg-color for "inner-primary" and "inner-secondary"
// currently I can do this by doing:
document.querySelectorAll('.inner-primary').forEach(ele => ele.style.setProperty('--bg-color', toggle ? 'blue' : 'yellow'))
document.querySelectorAll('.inner-secondary').forEach(ele => ele.style.setProperty('--bg-color', toggle ? 'yellow' : 'blue' ))
// another way I can see is: we dynamically insert a style tag, but this feels very awkward and can quickly get out of hand on multiple iterations
}
:root {
--bg-color: red;
}
body {
margin: 0;
}
.outer {
width: 100vw;
height: auto;
min-height: 100vh;
text-align: center;
background-color: var(--bg-color); /* receives value from :root */
}
.inner-primary,
.inner-secondary {
width: 100px;
height: 100px;
/* received scoped value from .inner-primary or .inner-secondary defined below*/
background-color: var(--bg-color);
border: 1px solid black;
margin: auto;
margin-bottom: 10px;
}
.inner-secondary {
--bg-color: yellow;
}
.inner-primary {
--bg-color: blue;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Testing</title>
</head>
<body>
<div class="outer">
<div class="inner-primary"></div>
<div class="inner-secondary"></div>
<div class="inner-primary"></div>
<div class="inner-secondary"></div>
<button onclick="changeColor()">Change Color</button>
</body>
</html>
Please try running this to get a full idea of intended effect. You can click "Change Color" button at the bottom to see the effects in action.
To get the intended overriding for CSS variable --bg-color for classes inner-primary and inner-secondary, I had to use querySelectorAll with the required CSS selector(in this case just a class name) and iteratively set the CSS variable for each individual element found.
By nature of how CSS gets read by the browser, feels like the other solution to this is to dynamically insert a style element tag into the DOM, with the required CSS variable update, scoped under the required class name(or any other required selector)..
But this feels awkward and can quickly get out of hand if we don't implement some system to reuse the same style tag and not insert new ones during each toggle.
Is there any other way to do this? Any native API which can solve this without having to access individual elements or without inserting style tags dynamically..?

As suggested by A Haworth and referring Change CSS of class in Javascript? I was able update changeColor function to use CSSStyleSheet(MDN link) instead. Please find the updated function below, which uses this API:
var toggle = true;
function changeColor() {
document.documentElement.style.setProperty('--bg-color', toggle ? 'green' : 'red');
// solution using document stylesheets
const styleSheet = document.styleSheets[0]
const cssRules = Array.from(styleSheet.cssRules);
const primaryClassIndex = cssRules.findIndex(cssRule => cssRule.selectorText === '.inner-primary');
const secondaryClassIndex = cssRules.findIndex(cssRule => cssRule.selectorText === '.inner-secondary');
//update primary:
styleSheet.deleteRule(primaryClassIndex);
styleSheet.insertRule(`.inner-primary {--bg-color: ${toggle ? 'yellow' : 'blue'};}`, primaryClassIndex)
//update secondary:
styleSheet.deleteRule(secondaryClassIndex);
styleSheet.insertRule(`.inner-secondary {--bg-color: ${toggle ? 'blue' : 'yellow'};}`, secondaryClassIndex)
//toggle
toggle = !toggle;
}
This is still some concern here since it seems like we can only overwrite the entire cssRule(which may also include other CSS properties) for a particular selector, and not just one required property. But this may arguably be better than updating each individual element style or inserting style tags as mentioned in the question.
Can check the full working codepen at => https://codepen.io/yadus/pen/mdWZmXX

Related

Combine :host() with :has() - not possible?

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...

From .class scope, get CSS custom property (variable) value using javascript

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>

binding textContent and overriding dom with svelte

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>

Q: Add Javascript(By Link To HTML File) To Custom Element With DOM

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.

How to change CSS :root color variables in JavaScript

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;
}

Categories

Resources