I'm trying to figure out a way to use JavaScript to dynamically create CSS rules on a page, modify them, and disable them. The rules would come in "packages" with an ID and a group of one or more CSS rules which may overlap.
/*ID: something*/
div#foobar {display:none;}
/*ID: blah*/
input {background:red;}
div.password {width:100px;}
/*ID: test*/
div.password {width:200px;}
The rules are in an associative-array that contains the data, such as:
myrules["something"] = "div#foobar {display:none;}";
myrules["blah"] = "div.password {width:100px;} input {background:red;}";
myrules["test"] = "div.password {width:200px;}";
Now I need a way to add the defined rules to the page with a way to toggle them using the IDs.
The main issues the current attempts (below) have run into are:
background-color
, not .backgroundColor
)I've looked at several different ways from document.styleSheets
to .sheet.cssRules[]
, from .innerHTML
to insertRule()
. I've become dizzy from trying to figure out what's what; it's such a quagmire, with poor examples. Sometimes I manage to use one technique to accomplish one aspect of it, but then another aspect won't work. I can't find a solution that satisfies all of the aforementioned requirements.
And searches are difficult because of the ambiguous nature of phrasing leading to incorrect search-results.
Surely there has to be an efficient way to do this, right? 🤨
One further approach is below, with explanatory comments in the code:
// using an arrow function modifyStyles() to handle the application and
// removal of the user-selected styles, this function takes one argument
// a reference to the Event Object (passed automagically from the
// EventTarget.addEventListener() call, later):
const modifyStyles = (evt) => {
// destructuring assignment, which takes the 'target' property
// from the Event Object, and then assigns the value of that
// property to the 'btn' variable (this is a personal preference
// and helps me to keep track of what interaction this function
// handles):
let {
target: btn
} = evt,
// here we use document.querySelector() to retrieve a specific element
// using a CSS selector, if that returns a falsey result then instead
// we create that <style> element and assign that created element to
// to the 'style' variable:
style =
document.querySelector("#user-custom-css-content") ||
document.createElement("style");
// we toggle the 'active' class on the clicked <button> element, if the
// class is already present it will be removed, if not present it will
// be added:
btn.classList.toggle("active");
// here we retrieve closest ancestor '.wrapper' element to the clicked
// <button>, and from their retrieve all '.active' elements; we use the
// spread (...) operator and Array literal to convert the returned
// NodeList to an Array:
let rules = [...btn.closest(".wrapper").querySelectorAll(".active")]
// we then use Array.prototype.map() to iterate over the Array
// of Element Nodes and create a new Array based on those Nodes:
.map(
// here we retrueve the value of the data-style custom attribute
// from the elements, and use that value as the property-name of
// the cssRules object (defined below), creating an Array of
// CSS rules:
(el) => cssRules[el.dataset.style]
)
// then set the text-content of the <style> element to be a string
// with each Array element being joined together by a new-line
// character:
style.textContent = rules.join("\n");
// a variable to determine if the <style> element has content,
// if all <button> elements are deselected and none have the
// 'active' class there will be no content in the Array that
// was joined; here we test that by assessing whether a the
// length of the text-content with leading and trailing
// white-space removed is greater than zero, this returns a
// Boolean true/false:
let hasContent = 0 < style.textContent.trim().length;
// here we check to see if the id of the <style> Object is
// NOT equal to the string, and that the <style> has content:
if ("user-custom-css-content" !== style.id && hasContent) {
// if the id doesn't match, we set the id:
style.id = "user-custom-css-content"
// and if there is content we append the <style> to the
// <head> of the document:
document.head.append(style);
// otherwise if there is no content:
} else if (false === hasContent) {
// we use Element.remove() to remove the empty <style>
// element:
style.remove();
}
}
// a sample of potential CSS rules contained within an Object:
const cssRules = {
something: "h1, .item:nth-child(odd) { opacity: 0.5; } figcaption { text-decoration: underline; }",
blah: "h1 { background: linear-gradient(90deg, transparent, cyan); }",
test: "img + * { color: rebeccapurple; font-weight: 100;} li:nth-child(even of .item) {border-inline-start: 2px solid fuchsia; padding-inline-start: 1rem; } img { clip-path: polygon(50% 0%, 80% 10%, 100% 35%, 100% 70%, 80% 90%, 50% 100%, 20% 90%, 0% 70%, 0% 35%, 20% 10%);}",
}
// retrieving a NodeList of <button> elements with the data-style custom
// attribute:
const buttons = document.querySelectorAll("button[data-style]")
// iterating over that NodeList - using NodeList.prototype.forEach():
buttons.forEach(
// binding the modifyStyles() function as the handler for
// the 'click' event on the <button> elements:
(btn) => btn.addEventListener("click", modifyStyles)
)
@layer base {
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
block-size: 100%;
}
body {
block-size: 100dvh;
padding-block: 0.5rem;
padding-inline: 1rem;
}
main {
border: 1px solid;
inline-size: clamp(20em, 80%, 1000px);
margin-inline: auto;
padding: 0.5rem;
}
ul,
ol,
li {
list-style-type: none;
}
section {
display: grid;
gap: 0.5rem;
grid-template-columns: [full-start text-start] 2fr [text-end fig-start] 1fr [fig-end full-end];
}
h1,
footer {
grid-column: full;
}
.spiel,
ul {
grid-column: text;
}
ul {
column-count: 2;
}
figure {
grid-column: fig;
grid-row: 2 / span 2;
img {
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
transition: clip-path 2s linear;
}
}
footer {
.controls {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
justify-content: space-between;
&>* {
flex: 1 0 auto;
}
button {
background: var(--active-color, lightgrey);
border: 1px solid;
border-radius: 10rem;
inline-size: 100%;
&.active {
--active-color: lightcyan;
}
}
summary {
display: block;
font-size: 1rem;
}
}
}
}
<!-- generic HTML laid out as a common card component: -->
<main>
<section>
<h1>Arbitrary title!</h1>
<p class="spiel">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam voluptate totam placeat. Call to action, so on and so forth: do the thing!
</p>
<ul>
<li class="item item-1">List item 1</li>
<li class="item item-2">List item 2</li>
<li class="item item-3">List item 3</li>
<li class="item item-4">List item 4</li>
<li class="item item-5">List item 5</li>
<li class="item item-6">List item 6</li>
<li class="item item-7">List item 7</li>
<li class="item item-8">List item 8</li>
<li class="item item-9">List item 9</li>
<li class="item item-10">List item 10</li>
<li class="item item-11">List item 11</li>
<li class="item item-12">List item 12</li>
</ul>
<figure>
<img src="//picsum.photos/id/20/300" alt="" />
<figcaption>Text related to the featured (demo) image.</figcaption>
</figure>
<footer>
<ul class="controls wrapper">
<li>
<!-- using custom data-style attributes to store the name of
the cssRules Object property that contains the custom
rules to apply: -->
<button type="button" id="style-1" class="style-option style-1" data-style="something">
Style 1
</button>
</li>
<li>
<button type="button" id="style-2" class="style-option style-2" data-style="blah">
Style 2
</button>
</li>
<li>
<button type="button" id="style-3" class="style-option style-3" data-style="test">
Style 3
</button>
</li>
</ul>
</footer>
</section>
</main>
References:
Array.prototype.join()
.Array.prototype.map()
.document.createElement()
.document.querySelector()
.document.querySelectorAll()
.Element.append()
.Element.classList
API.Element.closest()
.Element.remove()
.Node.textContent
....
) syntax.String.prototype.split()
.