Skip to content

Instantly share code, notes, and snippets.

@broofa
Last active January 21, 2024 17:22
Show Gist options
  • Save broofa/7e95aad7ea0f34655428cda9868e7fa3 to your computer and use it in GitHub Desktop.
Save broofa/7e95aad7ea0f34655428cda9868e7fa3 to your computer and use it in GitHub Desktop.
ES module for detecting undefined CSS classes (uses mutation observer to monitor DOM changes). `console.warn()`s undefined classes.
/**
* Sets up a DOM MutationObserver that watches for elements using undefined CSS
* class names. Performance should be pretty good, but it's probably best to
* avoid using this in production.
*
* Usage:
*
* import cssCheck from './checkForUndefinedCSSClasses.js'
*
* // Call before DOM renders (e.g. in <HEAD> or prior to React.render())
* cssCheck();
*/
const seen = new Set();
let defined;
function detectUndefined(node) {
if (!node?.classList) return;
node._cssChecked = true;
for (const cl of node.classList) {
// Ignore defined and already-seen classes
if (defined.has(cl) || seen.has(cl)) continue;
// Mark as seen
seen.add(cl);
console.warn(`Undefined CSS class: ${cl}`);
}
}
function ingestRules(rules) {
for (const rule of rules) {
if (rule?.cssRules) { // Rules can contain sub-rules (e.g. @media, @print)
ingestRules(rule.cssRules);
} else if (rule.selectorText) {
// get defined classes
const classes = rule.selectorText?.match(/\.[\w-]+/g);
if (classes) {
for (const cl of classes) { defined.add(cl.substr(1)); }
}
}
}
}
export default function init() {
if (defined) return defined;
defined = new Set();
ingestRules(document.styleSheets);
// Watch for DOM changes
const observer = new MutationObserver(mutationsList => {
for (const mut of mutationsList) {
if (mut.type === 'childList' && mut?.addedNodes) {
for (const el of mut.addedNodes) {
if (el.nodeType == 3) continue; // Ignore text nodes
// Check sub-dom for undefined classes
detectUndefined(el);
for (const cel of el.querySelectorAll('*')) {
detectUndefined(cel);
}
}
} else if (mut?.attributeName == 'class') {
detectUndefined(mut.target);
}
}
});
observer.observe(document, {
attributes: true,
childList: true,
subtree: true
});
}
@targos
Copy link

targos commented Oct 14, 2021

Thanks for the snippet! I improved it to support the tokens used by TailwindCSS classes:

function ingestRules(rules) {
  for (const rule of rules) {
    if (rule?.cssRules) {
      // Rules can contain sub-rules (e.g. @media, @print)
      ingestRules(rule.cssRules);
    } else if (rule.selectorText) {
      // get defined classes
      const classes = rule.selectorText?.match(/\.(?:[\w-]|\\[:./[\]])+/g);
      if (classes) {
        for (const cl of classes) {
          defined.add(cl.substr(1).replace(/\\/g, ''));
        }
      }
    }
  }
}

@broofa
Copy link
Author

broofa commented Oct 14, 2021

BTW, I've turned this into an NPM module: https://github.com/broofa/checkcss

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment