Check if CSS selector is valid

Since querySelector and CSS.supports('selector('+s+')') accept unclosed input like a[href="foo,
let's use the built-in CSSStyleSheet API to check the selector can be used in a CSS rule:

let isv;
function isSelectorValid(sel) {
  if (!isv) {
    try {
      // Chrome 73 and newer
      isv = new CSSStyleSheet();
    } catch (e) {
      // This will fail on sites with an unusually strict CSP that forbids inline styles,
      // so you'll need to set `nonce` or reuse an existing `link` element.
      isv = document.head.appendChild(document.createElement('style')).sheet;
      isv.disabled = true;
    }
  }
  let res = false;
  try {
    // the leading space skips selector's trailing escape char
    const body = ` { --foo: "${Math.random()}"; }`;
    isv.insertRule(sel + body);
    res = isv.cssRules[0].cssText.endsWith(body);
    isv.deleteRule(0);
  } catch (e) {}
  return res;
}

You can use a library to verify if the selector is valid, and probably get more details from parsing. Check the css selector parser.


The problem with original idea is that it will search the entire document. Slow 🤨 !

However, searching an empty light-weight element that is not even attached to the DOM is fast ✌️!

const queryCheck = (s) => document.createDocumentFragment().querySelector(s)

const isSelectorValid = (selector) => {
  try { queryCheck(selector) } catch { return false }
  return true
}

console.assert(isSelectorValid('p > > > a') === false)
console.assert(isSelectorValid('p > a') === true)
console.log('Test passed')

The following version is a bit more advanced with dummy fragment enclosure:

const isSelectorValid = ((dummyElement) =>
  (selector) => {
    try { dummyElement.querySelector(selector) } catch { return false }
    return true
  })(document.createDocumentFragment())
  
console.assert(isSelectorValid('p > > > a') === false)
console.assert(isSelectorValid('p > a') === true)
console.log('Test passed')

Thanks to @kornieff hint, I've reached out to an answer for nodejs using jsdom, if it can help anyone :

const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { document } = (new JSDOM('')).window;

const queryCheck = s => document.createDocumentFragment().querySelector(s)

const isSelectorValid = selector => {
  try { queryCheck(selector) } catch { return false }
  return true
}
console.log(isSelectorValid("a#x#y"), isSelectorValid("a?+"));