39

I know it is possible to add new CSS classes definitions at runtime through JavaScript. But...

How to change/remove CSS classes definitions at runtime?

For instance, supose a I have the class below:

<style>
.menu { font-size: 12px; }
</style>

What I want is, at runtime, change the font-size rule of the .menu class, so that every element in the page who uses this class will be affected.

And, I also want to know how to remove the .menu class definition.

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
Daniel Silveira
  • 41,125
  • 36
  • 100
  • 121

8 Answers8

26

It's not difficult to change CSS rules at runtime, but apparently it is difficult to find the rule you want. PPK has a quick tour of this on quirksmode.org.

You'll want to use document.styleSheets[i].cssRules which is an array you need to parse through to find the one you want, and then rule.style.setProperty('font-size','10px',null);

Jason S
  • 184,598
  • 164
  • 608
  • 970
19

I found an answer at http://twelvestone.com/forum_thread/view/31411 and I'm reproducing parts of the thread here, verbatim, because I'm afraid the thread, and the very helpful answer, will evaporate.

Flip 2006.06.26, 02:45PM — [ Crunchy Frog ] posts: 2470 join date: 2003.01.26

Well after about 10 to 12 hours of searching, reading, and tinkering I've done it! I am CSS/JS code Ninja today!

The JS code used is as follows:

<script language="JavaScript">
function changeRule(theNumber) {
    var theRules = new Array();
    if (document.styleSheets[0].cssRules) {
        theRules = document.styleSheets[0].cssRules;
    } else if (document.styleSheets[0].rules) {
        theRules = document.styleSheets[0].rules;
    }
    theRules[theNumber].style.backgroundColor = '#FF0000';
}
</script>

I've tested this on FF(Mac), Safari(Mac), O9(Mac), IE5(Mac), IE6(PC), FF(PC) and they all work. The reason for the 'if' statement is some of the browsers use cssRules... some use just rules... And the only other hair is that you can't use "background-color" to refer to the style, you have to get rid of the hyphen and capitalize the first letter after the hyphen.

To refer to the first CSS rule you'd use "changeRule(0)", the second "changeRule(1)" and the third "changeRule(2)" and so on...

I haven't found a browser it doesn't work on.... yet.... Anything you say can and will be used against you. Over and over and over.


BillyBones 2011.01.20, 11:57AM — [ in the barrel ] posts: 1 join date: 2011.01.20

Hello, I registered in these forums just to add this little bit as I could not conveniently find it elsewhere:

function changeStyle(selectorText)
{
    var theRules = new Array();
    if (document.styleSheets[0].cssRules) {
        theRules = document.styleSheets[0].cssRules;
    } 
    else if (document.styleSheets[0].rules) {
        theRules = document.styleSheets[0].rules;
    }
    for (n in theRules)
    {
        if (theRules[n].selectorText == selectorText)   {
            theRules[n].style.color = 'blue';
        }
    }
}

This simply makes the CSS rule identifiable by its selector name rather than by its index number in the cssRules array.

In other words, you can execute the Javascript function with the string argument "selectorText" instead of a number that is difficult to remember and susceptible to frequent changes if new styles are added.

Thank you for your 10 to 12 hours of research, Flip, I hope I made a worthy addition.

Pete Wilson
  • 8,610
  • 6
  • 39
  • 51
12

i think you are looking for this:

http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript

this lets you change the actual rules with javascript. ive used it once, a few years ago it seemed to have worked.

mkoryak
  • 57,086
  • 61
  • 201
  • 257
7

I've made a simple helper function for anyone that want to do that:

function getCSSRule(search) {
  return [].map.call(document.styleSheets, function(item) {
    return [].slice.call(item.cssRules);
  }).reduce(function(a, b) {
    return b.concat(a);
  }).filter(function(rule) {
    return rule.selectorText.lastIndexOf(search) === rule.selectorText.length - search.length;
  })[0];
}

And then, you can use it like that:

getCSSRule('.mydiv').style.fontSize = '20px';

Take a look at the example below:

function getCSSRule(search) {
  return [].map.call(document.styleSheets, function(item) {
    return [].slice.call(item.cssRules);
  }).reduce(function(a, b) {
    return b.concat(a);
  }).filter(function(rule) {
    return rule.selectorText.lastIndexOf(search) === rule.selectorText.length - search.length;
  })[0];
}

document.querySelector('button').addEventListener('click', function(e) {
  getCSSRule('.iframe').style.backgroundColor = 'orange';
});
.iframe {
  height: 200px;
  width: 200px;
  display: inline-block;
  border: 1px solid #000;
}
<p>
  <button>Change .iframe background-color</button>
</p>
<div class="iframe"></div>
<div class="iframe"></div>
Buzinas
  • 11,597
  • 2
  • 36
  • 58
  • 1
    This should be the answer – stenci Mar 03 '16 at 00:30
  • It wasn't working in some cases. I changed the third `return rule...` into `return rule.selectorText === search;` and now it seems to work always. – stenci Mar 03 '16 at 15:55
  • There are rule objects that do not have the selectorText property, e.g. `@font-face{..}`. Therefore I altered the `return rules.selectorText...` line to `return rule && rule.selectorText ? (rule.selectorText.lastIndexOf(search) === rule.selectorText.length - search.length) : -1;` – Humppakäräjät Mar 23 '16 at 13:12
  • 1
    in Pete Wilsons answer above it says 'some of the browsers use cssRules... some use just rules' . Does getCSSRule need to be changed to accommodate this? – Frazer Kirkman Nov 09 '16 at 13:55
  • 1
    @FrazerKirkman—[*according to MDN*](https://developer.mozilla.org/en-US/docs/Web/API/CSSRule), IE9+ supports *CSSRules*. Lower than IE 9 doesn't support *map* or *filter* and will barf on `[].slice.call(item.cssRules)` since (from memory) it doesn't allow using *call* to set *this* to a host object. `[].map.call` will work OK since *map* (if present on – RobG Jan 30 '17 at 06:03
  • 2
    This answer, and the fixes listed, does not work for many use cases in 2020. I found @FrazerKirkman 's link, (https://github.com/Frazer/dynamicallyAccessCSS.js), privided on the accepted answer above, does. It works perfectly. – Nick Steele May 18 '20 at 17:52
  • 1
    @NickSteele, perhaps you could upvote my answer, so that people notice it? – Frazer Kirkman May 27 '21 at 01:00
  • 1
    Sure, you got it! :) – Nick Steele May 27 '21 at 02:27
2

I took the best of the answers here, and combined them, for ease of use, and cross browser compatibility. Also, I covered when threw errors if no stylesheets were on the page, or if the css rule did not exist yet.

https://github.com/Frazer/dynamicallyAccessCSS.js

Frazer Kirkman
  • 1,003
  • 1
  • 14
  • 23
1

Here's an embarrassingly simple trick I've been using for dynamically manipulating CSS class rules, which dodges the complications (like parsing through the rules to locate the one you need, as noted by the expected answer), and even provides some extra flexibility for free.

(A side-note: the prev. attempt of adding this answer got instanty downvoted, within seconds, without feedback, by one of the few auto-notification targets of this page. I'm not sure the short knee-jerk reaction time was enough to see why this is a reliable solution that covers the OP's use case well, nevertheless I've rephrased it, guessing the narrative may have somehow been the trigger. Or the lack of semicolons (which I've now added for him). Or the lack of unnecessary curly braces (also added). Or the lack of notes on non-effects (added). Or not using a sufficiently complicated method (won't fix, even simplified the code yet some more). I'm sure he'll hit again anonymously, but still retry, because this is a clean & robust technique worth using.)

NOTE: This approach assumes you have sufficient control over the styling of the page to make sure the same class rule would not get created by a different method. (And it expects the HEAD element to exist.)

(One potential cost of this approach could be re-rendering on innerHtml, but since we are changing CSS anyway, repaint is imminent. Also, innerHtml is done to a non-displayed element, so browsers could optimize it out even if it otherwise mattered.)

OK. Since you can have any number of STYLE elements, it's perfectly fine to wrap your dynamic class in its own separate one. Um, that's it. :) Then, add an id or, even better*, a class attribute to the wrapper STYLE, so you can practically access and manipulate your class rule as if it was a DOM element. Just make sure it's wrapped in <style class="..."> ... </style>, when adding/replacing.

Bonus: you can also group multiple related rules and replace them all at once this way, if you wish (with trivial modifications):

function replace_class(classname, block) {
    // Remove old:
    var s = document.head.querySelector("style." + classname);
    if (s) { document.head.removeChild(s); }

    // Just delete?
    if (!block) { return; }

    // Add new:
    s = document.createElement("style");
    s.className = classname;
    s.innerHTML = ("." + classname + block); // <style class="classname">.classname{...}</style>
    document.head.appendChild(s);
}

Then, to update: replace_class("menu", "{font-size: 8px;}").

Or delete: replace_class("menu", null).


* Since CSS applies to every element, you may wonder why won't STYLE itself get unexpectedly rendered, if your new class had a display: ... with something else than none. Well, it would, if it was put in the BODY! But, since we add it to HEAD, rendering is skipped (unless, of course, you opt to display HEAD, too). Or, you could also use id instead, but then you had to invent/use proper scoping/prefixing, and why would you want that if it can be spared? (And I also like the subtle cheekiness of setting class when it's a class wrapper anyway...)

Sz.
  • 3,342
  • 1
  • 30
  • 43
0

It is difficult to find the rule you want because you have to iterate through the document.styleSheets[i].cssRules array. (and compare your class name with the selectorText attribute) So my solution to this problem is to add a new CSS class, remove the old CSS class from the HTML element and add this class instead of it.

var length = getCssRuleLength();
var newClassName = "css-class-name" + length;

//remove preview css class from html element.
$("#your-html-element").removeClass("css-class-name");
$("#your-html-element").removeClass("css-class-name" + (length-1));

$("#your-html-element").addClass(newClassName);

//insert a css class
insertCssRule("." + newClassName + ' { max-width: 100px; }', length);


function getCssRuleLength() {
 var length = 0;
 if (document.styleSheets[1].cssRules) {
  length = document.styleSheets[1].cssRules.length;
 } else if (document.styleSheets[1].rules) { //ie
  length = document.styleSheets[1].rules.length;
 }
 return length;
}
function insertCssRule(rule, index) {
 if (document.styleSheets[1].cssRules) {
  document.styleSheets[1].insertRule(rule, index);
 } else if (document.styleSheets[1].rules) { //ie
  document.styleSheets[1].addRule(rule, index);
 }
}
0

Below is an approach that will work for any given rule-selector and rule-change function:

// general function for selecting rules and applying changes
function change_css_rules(changeRuleFunc, selectorFunc) {
    [].concat.apply([], // flattens arrays
        Array.from(document.styleSheets).map(function(sheet) { // each ss
            return Array.from(sheet.cssRules).filter(function(rule) { // each rule
                return selectorFunc(rule); // only select desired rules
            });
        })
    ).map(changeRuleFunc); // change the selected rules
}

Example use:

var my_changeRuleFunc = function(rule) {
    rule.style.fontSize = '20px';
}

var my_selectorFunc = function(rule) {
    return rule.selectorText == '.myClass'; // return true to select this rule
}

change_css_rules(my_changeRuleFunc, my_selectorFunc); // apply change to selected rules
mwag
  • 3,557
  • 31
  • 38