How do you use JavaScript to change CSS styles? This seems like an obvious question with an obvious answer: 'modify your site's stylesheets – potentially followed by a compilation step – then update styles at runtime via changing element attributes such as class
and aria-*
'.
Yet the answer is not complete, consider the following:
- How do you update styles at run-time based on user interaction instead of a preset value? It's not feasible to programmatically generate unique class names for every possible color combination.
- What if you do not have modification access to stylesheets or HTML? Such as a site builder generated site with third-party CSS files littered with
!important
properties.
In this post, I'll go over 4 methods of style updates using JavaScript and their use cases. We'll also consider each method's usefulness in terms of its CSS Complexity. That is, how easy is it to understand and modify.
Use Inline Style
Before the invention of CSS, legacy HTML attributes such as color
, background
, and border
were used to style web pages. Inline CSS is the spiritual successor of these attributes, where CSS properties are controlled via the element's style
attribute.
The following two ways of changing font size on a hero element are equivalent:
document.getElementById('hero').style = 'font-size: 12rem;';
document.getElementById('hero').style.fontSize = '12rem';
Using JavaScript to update inline CSS is generally considered bad practice for the following reasons:
- It violates the separation of concerns between style and content, making the document hard to read and modify.
- It removes the ability of CSS selectors to form semantic abstractions.
- Without selectors, updating multiple elements on the page requires iterating through each element – a slow and error-prone process.
- It bloats your HTML via repeated styles.
- It does not have access to pseudo-elements and pseudo-classes that are only available via CSS selectors.
We are currently seeing a resurgence of inline CSS-lite via atomic CSS frameworks such as Tailwind CSS – see my previous posts on Tailwind. Atomic frameworks use CSS class names that translate to roughly one CSS property and rely on JS generate components to increase style reuse. This overcomes shortcomings 3, 5, and 4 from above – though I think the HTML bloat is still considerable.
While inline is CSS generally harmful, it has the benefit of not requiring stylesheet access to make style updates and can be used to make arbitrary run-time style changes.
When to modify inline-CSS: Run-time update of a single element style on the page, for quick dev tool tests, and when stylesheet access is unavailable.
Use HTML Attributes
Next up, we have the canonical answer of modifying element attributes other than style. It is the most popular for a good reason, it does a great job at creating reusable and semantically meaningful styles. Here are some examples:
// toggles HTML semantic state
document.getElementById('cta').disabled = true;
// a aria based semantic button state
document.getElementById('cta').ariaExpanded = "true";
// a class based semantic primary variation
document.getElementById('cta').classList.toggle('primary');
Not much to add here, you are probably both familiar with and use this method extensively. It's both easy to understand and modify, and we have plenty of CSS methodologies to control its complexity.
I do want to point out that the examples above listed are in their order of meaningfulness. We should give precedence to HTML attribute-based states before leaning on class-based states – easier now that :has()
selector is on the horizon.
The only downsides of the method are the two I mentioned in the introduction: that it is not possible to generate all arbitrary CSS styles ahead of time, and you need to be able to modify all stylesheets on the site.
When to modify (non-style) attributes: all cases when you have access to stylesheets and have pre-defined styles.
Use CSSOM
The next method for JavaScript-based CSS modification is the surgical knife of the front-end toolkit: directly modifying CSS stylesheet objects. While the previous two methods modify the HTML DOM to affect the style, in some instances it is easier to directly change the CSS Object Model (CSSOM) instead.
By accessing the document's styleSheets
object, we can make arbitrary changes to a site's styles with the full power of CSS. For example:
const thirdPartyStylesheet = document.styleSheets[0];
//index 15 rule color: red !important;
thirdPartyStylesheet.deleteRule(15);
You can even add new dynamically generated stylesheets to the site via the CSSStyleSheet
constructor. In my experience, this is the best way to deal with third-party CSS stylesheets or site builders with limited CSS capabilities.
The CSSOM approach avoids the dreaded CSS selector escalation where you litter inline style with !important
to override third-party styles. It can also be more performant than looping through multiple elements to dynamically update their styles.
The main drawback of the CSSOM approach is how difficult it is to understand and debug. Dev-tool support for modified CSSOM is lacking and unless documented in 24-point font, it can drive future maintainers crazy. Like a surgical knife, use it sparingly and provide plenty of warning.
When to modify CSSOM: best used to remove unwanted third-party styles instead of adding new styles, good for changing styles you have no control over.
Use CSS Custom Properties
The last approach for dynamically updating CSS styles is through CSS custom properties. Though it technically does not use any new APIs, using custom properties is sufficiently different from the previous approaches that it deserves mentioning.
The custom properties can be used with any of the previous methods:
const themeColor = document.getElementById('color-picker').value;
// use with inline style
document.body.style=`--theme-color: ${themeColor};`;
// use in CSSOM
const stylesheet = document.styleSheets[0];
stylesheet.insertRule(`:root { --theme-color: ${themeColor}; }`);
An element's CSS custom properties are inherited by its children. We can use them with inline styles and not worry about selecting and looping through all elements in DOM – we just need to find their shared ancestor. Because of this, custom properties can also be used to modify pseudo-elements with inline style.
The biggest downside of using custom properties is they require planning and stylesheet access. When used judiciously, they can modify multiple styles with a single style update, such as generating an entire color palette with one color update.
Compare to the previous approaches, custom properties require as much if not more planning than the element attribute approach, but can be used for run-time style updates. It's easier to maintain than the CSSOM approach– there are fewer changes to keep track of – but you need stylesheet access.
When to use CSS Custom Properties: When you need to make complex run-time style changes, when you want to create new relationships between styles properties, or when you need to pierce the Shadow DOM to style lots of web components.
Take-Aways
Next time you need to change CSS style via Javascript, ask yourself:
- Is this a pre-defined style update or is its value determined dynamically at run-time?
- Am I overriding an existing third-party style?
- Do I need to modify a single element, or multiple ones on the page, including pseudo-elements and classes?
- Do I want this change to affect multiple derived properties, or shared by multiple elements on the page?
Front-end development is evolving quicker than ever and with no signs of slowing down. It's helpful to periodically slow down and take a look at all the tools available in our front-end toolkit.
And speaking of new tools, I am especially looking forward to learning and using typed style properties through the Houdini custom properties API.