Messing with CSS through its JavaScript API
Arek Nawo
Posted on May 29, 2019
This post is taken from my blog, so be sure to check it out for more up-to-date content.
Beyond all preprocessors, transpiler, or whatever web development tool you can think of, one fact still remains true - it's HTML, CSS, and JavaScript that power today's web. Any kind of tool, language and etc., all that remains dependent on these 3 technologies (if we don't count the uprising WebAssembly). They work and interact together, to provide you with limitless possibilities to create newer, better and even more stunning things!
JavaScript is - if we can call it that way - the king of interactivity. Its capabilities as a scripting language itself, combined with numerous web APIs extending its feature-set even further, are truly impressive. Examples of such APIs include the most well-known WebGL API, Canvas API, DOM API, but also a lesser-known set of CSS-related methods, which can be called (unofficially) CSS API. And that's what we're going to explore in today's post!
While the idea of interacting with DOM through its JS API was made really popular thanks to concepts such as JSX and countless JS frameworks, the use of similar techniques with CSS doesn't seem to have that much attention. Of course, CSS-in-JS solutions exist, but the most popular ones are rather based on transpilation, outputting CSS without any additional runtime in production. That's good for performance for sure, as CSS API usage may cause additional reflows, which makes it just as demanding as the use of DOM API. But still, this isn't what we're looking for. What if I tell you that you can not only manipulate DOM elements' styles and CSS classes but also create full-blown stylesheets, just like with HTML, just with the use of JS?
Basics
Inline styles
Before we'll dive deep into the complex stuff, let's first remind ourselves some basics. Like the fact that you can edit the given HTMLElement
's inline styles by modifying its .style
property.
const el = document.createElement("div");
el.style.backgroundColor = "red";
// or
el.style.cssText = "background-color: red";
// or
el.setAttribute("style", "background-color: red");
Setting your style properties directly on the .style
object will require you to use camelCase as your property keys, instead of kebab-case. If you have much more inline style properties to set (although, in such case, you may consider using CSS classes), you can do this in a bit more performant way, by setting the .style.cssText
property or by setting the style
attribute. Keep in mind that this will completely reset the inline styles of your element, and thus, requires you to include all properties (even the unchanged ones) in the string argument. If such micro-optimizations don't interest you (they really shouldn't), and your targeting modern browsers, you can consider using .style
with Object.assign()
, to set multiple style properties at once.
// ...
Object.assign(el.style, {
backgroundColor: "red",
margin: "25px"
});
There's a bit more to these "basics" than you'd probably think of. The .style
object implements the CSSStyleDeclaration
interface. This means that it comes with some interesting properties and methods! This includes known to us .cssText
, but also .length
(number of set properties), and methods like .item()
, .getPropertyValue()
and .setPropertyValue()
, allowing you to operate on inline styles, without the use of camelCase, and thus - any case conversion. You can find the complete API documented on MDN.
// ...
const propertiesCount = el.style.length;
for(let i = 0; i < propertiesCount; i++) {
const name = el.style.item(i); // e.g. "background-color"
const value = el.style.getPropertyValue(name); // e.g. "red"
const priority = el.style.getPropertyPriority(name); // e.g. "important"
if(priority === "important") {
el.style.removeProperty();
}
}
Just a small tidbit - the .item()
method that's most useful during iterations, has the alternate syntax in the form of access by index.
// ...
el.style.item(0) === el.style[0]; // true
CSS classes
Now, let's leave inline styles for a moment and take a look at higher structures - CSS classes. The basics include the .className
which has a form of a string when retrieved and set.
// ...
el.className = "class-one class-two";
el.setAttribute("class", "class-one class-two");
Another way of setting classes string is by setting the class
attribute (same for retrieval). But, just like with .style.cssText
property, setting .className
would require you to include all classes of the given element in the string, including the changed and unchanged ones. Of course, some simple string operations can do the job, but surely there has to be another way... And there is! It's provided to us in the form of slightly-newer .classList
property. By "slightly newer" I mean that it isn't supported by IE 9, and only partially supported by IE 10 and IE 11.
The .classList
property implements DOMTokenList
, giving you access to a whole bunch of useful methods. Likes of .add()
, .remove()
, .toggle()
and .replace()
allow you to change the current set of CSS classes, while others, e.g. .item()
, .entries()
or .forEach()
simplify the iteration process of this index collection.
// ...
const classNames = ["class-one", "class-two", "class-three"];
classNames.forEach(className => {
if(!el.classList.contains(className)) {
el.classList.add(className);
}
});
Stylesheets
Now that we're done with the revision, let's start creating our JS-only stylesheet! First, let's break down all the details behind what's going on.
Going from top to the bottom, we have the StyleSheetList
interface, implemented by document.styleSheets
property. It helps to represent the situation seen in standard HTML code - the use of multiple stylesheets in one document. Whether it's from an external file, URL or within <style/>
tag, document.styleSheets
collects them all in an indexed collection, implementing standard iteration protocols. With that said, you can access all the CSSStyleSheet
s with a simple loop.
for(styleSheet of document.styleSheets){
console.log(styleSheet);
}
As that's all there is to StyleSheetList
, let's go over to CSSStyleSheet
itself. It's here where things start to get interesting! CSSStyleSheet
extends StyleSheet
interface, and, with this relation come only a few read-only properties, like .ownerNode
, .href
, .title
or .type
, that are mostly taken straight from the place where given stylesheet was declared. Just recall the standard HTML code for loading external CSS file, and you'll know what I'm talking about.
<head>
<link rel="stylesheet" type="text/css" href="style.css" title="Styles">
</head>
So, all the stuff that interests us the most is inside the CSSStyleSheet
interface. Now, we know that HTML document can contain multiple stylesheets, and now... all these stylesheets can contain different rules or even more stylesheets (when using @import
) within them! And that's the point we're at. CSSStyleSheet
gives you access to two methods - .insertRule()
and .deleteRule()
.
// ...
const ruleIndex = styleSheet.insertRule("div {background-color: red}");
styleSheet.deleteRule(ruleIndex);
These methods operate with indices and CSS-like strings. As CSS rules order is important to decide which one should be used in case of conflict, .insertRule()
allows you to pass an optional index for your new rule. Know that some misuses may result in an error, so... just keep it simple.
CSSStyleSheet
also has two properties of its own - .ownerRule
and .cssRules
. While .ownerRule
is related to @import
stuff, it's the second one - the .cssRules
- that interests us the most. Simply put, it's a CSSRuleList
of CSSRule
s, that can be modified with earlier-mentioned .insertRule()
and .deleteRule()
methods. Keep in mind, that some browsers may block you from accessing .cssRules
property of external CSSStyleSheet
from a different origin (domain).
So, what about CSSRuleList
? Again, it's an iterable collection of CSSRule
s, meaning that you can iterate over it, access its CSSRule
s by their indices or .item()
method. What you cannot do though is modifying CSSRuleList
directly. It can only be done with previously mentioned methods and nothing else.
The CSSRuleList
contains object implementing CSSRule
interface. This one comes with properties such as .parentStyleSheet
and - most importantly - .cssText
, containing all the CSS code of the given rule. There's yet one more interesting property - .type
. It indicates the type of given CSSRule
, according to specified constants. You should remember, that besides the most often use "standard" style-related rules, CSS can consist of e.g. @import
or @keyframes
(most notably) rules. CSSRule
s of different types have corresponding interfaces. As you won't be creating them directly, but rather with CSS-like strings, you don't really have to know anything more, that the properties that these extended interfaces provide.
In case of the CSSStyleRule
, these properties are .selectorText
and .style
. First one indicates the selector used for the rule in the form of a string, and the second one is an object implementing CSSStyleDeclaration
interface, which we've discussed before.
// ...
const ruleIndex = styleSheet.insertRule("div {background-color: red}");
const rule = styleSheet.cssRules.item(ruleIndex);
rule.selectorText; // "div"
rule.style.backgroundColor; // "red"
Implementation
At this point, I think we know enough about CSS-related JavaScript APIs, to create our own, tiny, runtime-based CSS-in-JS implementation. The idea is that we'll create a function, that passed a simple style configuration object, will output a hashed name of newly created CSS class for later use.
So, our workflow here is pretty simple. We need a function that has access to some sort of stylesheet and just use .insertRule()
method together with phrased style config to make everything ticking. Let's start with the stylesheet part.
function createClassName(style) {
// ...
let styleSheet;
for (let i = 0; i < document.styleSheets.length; i++) {
if (document.styleSheets[i].CSSInJS) {
styleSheet = document.styleSheets[i];
break;
}
}
if (!styleSheet) {
const style = document.createElement("style");
document.head.appendChild(style);
styleSheet = style.sheet;
styleSheet.CSSInJS = true;
}
// ...
}
If you're using ESM or any other kind of JS module system, you can safely create your stylesheet instance outside the function and not worry about other people accessing it. But, as I wanted to keep this example minimal, we'll just set the .CSSInJS
property on our stylesheet as a form of a flag, informing us if this is the one we want to use.
That's pretty much all about the first part of the code snippet above. Now, what if we have to create a new stylesheet for our purposes? There's no straight-forward way of doing this. Our best bet would be to create a new <style/>
tag and append it to our HTML document's <head/>
section. This automatically adds a new stylesheet to the document.styleSheets
list and allows us to access it by the .sheet
property of our <style/>
tag. Pretty clever, huh?
function createRandomName() {
const code = Math.random().toString(36).substring(7);
return `css-${code}`;
}
function phraseStyle(style) {
const keys = Object.keys(style);
const keyValue = keys.map(key => {
const kebabCaseKey =
key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
const value =
`${style[key]}${typeof style[key] === "number" ? "px" : ""}`;
return `${kebabCaseKey}:${value};`;
});
return `{${keyValue.join("")}}`;
}
Actually, beyond the tiny tidbit above, there's really no more similarly-interesting stuff going on. Naturally, we first need a way to generate a new, random name for our CSS class. Then, we need to properly phrase our style object, to the form of viable CSS string (or at least part of it). This includes the conversion between camelCase and kebab-case, and, optionally, handling of pixel unit(px) conversion. Oh, and don't forget the semicolon (;
) at the end of every key-value pair!
function createClassName(style) {
const className = createRandomName();
let styleSheet;
// ...
styleSheet.insertRule(`.${className}${phraseStyle(style)}`);
return className;
}
Then, we go to our main function and make the required adjustments. We generate the random name and insert the CSS rule to the stylesheet. As all out rules are about classes, they all require a dot on their respective beginning for a proper selector. Believe me, it's super easy to forget!
const redRect = createClassName({
width: 100,
height: 100,
backgroundColor: "red"
});
el.classList.add(redRect);
With all set and done, we can finally put our code to the final test! Everything should work just fine! Below is a CodePen to prove that.
What do you think?
As you can see, manipulating CSS from JavaScript level is very interesting. Whether you know it's possible or not, you must admit - it's pretty awesome. Our little example above is only a proof-of-concept. There's a lot more potential within CSS API (or rather APIs). And it's just waiting to be unveiled!
So, what do you think of this post? I'd love to see your opinions, comments, and reactions below! Also, if you like articles like this one, consider following me on Twitter, on my Facebook page, and checking out my personal blog. Again, thank you very much for reading this one, and I hope you're having a wonderful day!
Posted on May 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 25, 2024