javascripthtmlcssdomemotion

Why rules inserted in an empty style tag using insertRule get removed on resetting textContent?


var styletag = document.createElement("style"); 
document.head.appendChild(styletag);
styletag.appendChild(document.createTextNode('')); // This is needed to replicate issue
styletag.sheet.insertRule("#inner {  background-color: red;} ");
setTimeout(()=>styletag.textContent='',1000)
<span id="inner">content</span>

This particular piece of code adds the appropriate styling to the #inner element. Until the setTimeout code runs and textContent is reset to the empty string.
Then the stylesheet's rules (styletag.sheet.cssRules) become empty and the background-color is removed.
The third line is needed to replicate this issue for some reason.

var styletag = document.createElement("style"); 
document.head.appendChild(styletag);
// styletag.appendChild(document.createTextNode('')); // This is needed to replicate issue
styletag.sheet.insertRule("#inner {  background-color: red;} ");
setTimeout(()=>styletag.textContent='',1000)
<span id="inner">content</span>
Is there any explanation for why rules are getting removed?

I could have understood if i used something like

styletag.textContent = "#inner {  background-color: red;} "

instead of

styletag.sheet.insertRule("#inner {  background-color: red;} ");

Here resetting textContent to empty would remove the rules, which makes sense.
Maybe setting textContent makes the browser re-evaluate the CSSRules? But if so why is the issue only replicable when I append an empty text node to the <style> tag?


Solution

  • This is quite complex and honestly quite surprising, but the gist of the issue is that,

    A <style> element will update its rules based on its content in 3 cases:

    • The element is popped off the stack of open elements of an HTML parser or XML parser.

    • The element is not on the stack of open elements of an HTML parser or XML parser, and it becomes connected or disconnected.

    • The element's children changed steps run.

    In our case, the first one doesn't matter, since we're creating the element dynamically and not from a parser.

    The second one is only of limited importance too, it happens when we append the element in the DOM, but we don't really care of what it was before. Only thing is, when in the DOM we have a first sheet that got computed out of the current (empty) content.

    Then we've got the element's children changed one. This algorithm is called when a node is inserted (step 9) or removed (step 21). So when you do append the empty TextNode, this step is ran, and the <style> will update its rules again, with no content, again... That update isn't really important either. What's important though is that now, our <style> does have a child node.
    Indeed, when running the textContent setter steps with an empty string, the previous children of the target element are removed, no new node is added but the remove steps are still ran. However when there is no previous child in the target element, no node is removed and no new node is added. Neither the insert steps nor the remove ones are run, and we never reach that children changed steps.

    So it all boils down to how textContent = "" runs differently when there is previous content or not.