12

Really simple: how do I most accurately test if a browser has support for a certain CSS selector?

I currently have some CSS code that makes the page a little more interactive by using the :checked selector in CSS, but I want to create a fallback script that does the same thing with JavaScript, but only if the user's browser has no support for the :checked selector.

My question is, how do I most accurately test if the user's browser supports a certain CSS selector?

Here is the code I'd like to use it on:

HTML:

<label class="coolbox">
    <input type="checkbox"/>
    <span>I want to eat some caek.</span>
</label>

CSS:

.coolbox input {display:none;}
.coolbox span::before {
    content: "";
    display:inline-block;
    width:10px;
    height:10px;
    margin-right:5px;
    border:1px solid black;
}
.coolbox:hover span::before {border:1px solid #555;}
.coolbox:active span::before {border:1px solid #999;}

.coolbox span::before {background-color:#F77;}
.coolbox input:checked + span::before {background-color:#4A4;}

Demo

PS: I'd prefer not to just use conditional comments, because I'd like to follow the standard of detecting features instead of browsers.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Joeytje50
  • 18,636
  • 15
  • 63
  • 95
  • 1
    Isn't this what http://modernizr.com/ does? – j08691 Jan 13 '14 at 15:22
  • 3
    I'd prefer it if it would be possible with just a simple script instead of having to download a library which I'd just be using once. – Joeytje50 Jan 13 '14 at 15:24
  • You can check the [source](https://github.com/Modernizr/Modernizr) or [build your own custom modernizr version](http://modernizr.com/download/) with just the tests you want/need – Andreas Jan 13 '14 at 15:30
  • 1
    @Andreas Is `:checked` test available in modernizr? – Wesley Murch Jan 13 '14 at 15:31
  • You can write some script for IE <= 9 but you may need an special comment to make the load – DaniP Jan 13 '14 at 15:32
  • 1
    @Danko See the OP's "PS". – Wesley Murch Jan 13 '14 at 15:32
  • 1
    Checked seems to be available in modernizr: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/checked.js The `testStyles` method is basically `injectElementWithStyles`: https://github.com/Modernizr/Modernizr/blob/master/src/injectElementWithStyles.js – Tallmaris Jan 13 '14 at 15:36
  • 1
    `:checked` isn't available in the builder yet [source](https://github.com/Modernizr/Modernizr/pull/879) but the code is (see the link of @Tallmaris) – Andreas Jan 13 '14 at 15:40
  • @WesleyMurch I know I just was point that option could be the best without using tools like modernizr. Other way you may need to make a script that evaluates browser and version (if that's possible)... – DaniP Jan 13 '14 at 15:42

5 Answers5

9

You could use querySelector:

function testSelector(selector, node){
  var scope = document.createElement("div");
  scope.appendChild(node);

  try {
    return scope.querySelector(selector) !== null;
  } catch(e) { return false; }
}

You can test it like this:

var node = document.createElement("input");
node.type = 'checkbox';
node.checked = 'checked';

testSelector("input:checked", node); // === true

See this other question for more info on querySelector.

Community
  • 1
  • 1
Matt
  • 800
  • 1
  • 7
  • 15
  • If I run this script, there seems to be an error in `testSelector`'s `return` statement ("invalid argument"). I'm still looking for what is causing this exactly on google, but if that can be fixed, I think this'd be the best solution. Would you have any idea what could be the cause of this? – Joeytje50 Jan 13 '14 at 16:32
  • After testing `document.body.querySelector(':checked')` in IE11's IE8-emulator's console, it gave the same "invalid argument" error. It looks like IE8 (atleast in IE11 emulation mode) considers the `:checked` selector invalid, and throws an error. – Joeytje50 Jan 13 '14 at 16:39
  • I tested on Chromium and Firefox but not IE. I'm not sure what could be causing "invalid argument". If you're testing on IE, make sure you're using IE8+ in standards mode. Or the querySelector function will not exist. – Matt Jan 13 '14 at 16:43
  • 1
    I've decided to go with this answer, since this doesn't require any changes to either the CSS or JS, and doesn't modify the contents of the page to test anything. I've put a `try {} catch(e){}` around the part that was causing problems in IE8. I'm just going to assume that if the browser throws an error if you use `:checked` in `.querySelector`, the browser doesn't support it. – Joeytje50 Jan 13 '14 at 16:45
  • 2
    @joeytje50 is correct. An unrecognized or invalid selector should throw an error, not just return a null result, so IE is behaving correctly. When `querySelector()` returns null, that means the selector is valid but doesn't match anything. See http://www.w3.org/TR/selectors-api/#processing-selectors – BoltClock Jan 14 '14 at 11:45
  • I added a try/catch block in the solution to support selectors considered invalid. – Matt Jan 14 '14 at 12:47
  • It's much simpler to just run the selector string through `querySelector()` and return true without a null check, or false on error. As long as the browser doesn't throw an error, the selector is supported. In fact, what is really being tested here is not "if the browser supports the given selector", but "if the browser is able to find an element matching the given selector". You would need to supply an element that is guaranteed to match the selector in order for the test results to be reliable. – BoltClock Sep 19 '14 at 03:01
5

Workaround for your case:

<input type=checkbox checked=checked>

css:

input{
  font-family:'arial';
}
input:checked{
  font-family:'sans-serif';
}

checking procedure: js

alert($('input').css('font-family')=='sans-serif'?'supported':'not supported');
3

From some research I was able to find various websites that can test your browser to see the support.

This website is helpful to find what supports what but does not test your current browser.

This website will test your browser and if you don't want to use modernizr you can learn from their script.

This last website seems to do a great job and is very accurate of the support. I also am pretty sure I found the script that is doing this so like I said you can learn from how other website are doing this.

To figure how they are making this work you will need to understand how their scripts are working. The bottom three seem to be the most critical to the function.

<script src="utopia.js"></script>
<script src="supports.js"></script>
<script src="csstest.js"></script>
<script src="tests.js"></script>

These are just some options that I found and it is up to your needs. Best of luck and hopefully this has been helpful.

Josh Powell
  • 6,219
  • 5
  • 31
  • 59
  • Going by the source code on css3.info, I've made a little test script for use in the console: `var x = new CSSTestCase(['checked'], { onSuccess: function() {console.log(true,arguments)}, onFailure: function() {console.log(false,arguments)}});`, and when I browse through the logged message in the console, this script seems to require loading an iframe for every test. For the other site, I'm still looking, but it looks a bit confusing as to how they actually do the feature testing on their site. – Joeytje50 Jan 13 '14 at 16:07
  • @joeytje50 Hmm very weird, after looking through the page's source code there is no iframe anywhere on the page. I'm guess it must be generated in by the script which shouldn't be to much of a problem, you'll just have to find how the iframe is being added. **(nevermind I am talking about css3test, which seems like a better script)** – Josh Powell Jan 13 '14 at 16:10
2

A shorter way to do it would be to simply try the query selector, if it produces an error, return false, else true, like this:

function testSelector(selector) {
  document.querySelector('*');  //checks if querySelector is implemented and raises an error if not
  try {document.querySelector(selector)} catch (e) {return false}
  return true;
}

I checked it on IE9 Windows and Chrome Mac (V43.0.2357.130), Win(V39.0.2171.95m), FireFox Win (V38.0.5), and it works fine with testSelector("form:invalid"), which is not implemented by IE9, but by everybody else.

yogibimbi
  • 588
  • 5
  • 17
0

While this is, admittedly, a very late answer to a rather old question it seemed worth adding another answer.

One approach, using JavaScript to test for selector support, is below with explanatory comments in the code:

// some DOM utilities and helpers,
// caching document as I don't enjoy typing that much:
const D = document,
  // aliasing document.querySelector() and element.querySelector()
  // again, because I don't enjoy typing; here the function takes
  // two arguments 'sel' and 'context',
  // 'sel' is a String, and is the selector for the element we're
  // trying to retrieve;
  // 'context' is the element/node we wish to search within; if
  // no context is passed we default to document.querySelector()
  // otherwise we use Element.querySelector():
  get = (sel, context = D) => context.querySelector(sel),
  // as above, except it's an alias for querySelectorAll(), and
  // here we return an Array of nodes instead of a NodeList in
  // order to use Array methods when/if required:
  getAll = (sel, context = D) => [...context.querySelectorAll(sel)],
  // alias for document.createElement(), which also allows properties
  // to be set on the created element:
  create = (tag, prop) => document.createElement(tag),
  // simple function to allow for more elaborate templates, and
  // arguments if required later:
  templatedResult = (text) => `<code>${text}</code>`,
  // named function to assess whether the browser supports/implements
  // a given selector; this takes a reference to the Event Object
  // passed automatically from EventTarget.addEventListener():
  verifySelectorCompatibility = (evt) => {
    // preventing default actions (as one of the events to which
    // bind the function is form.submit):
    evt.preventDefault();

    // gathering variables/data
    // here we retrieve the first/only <input> element within
    // the document:
    const input = get('#testSelector'),
      // we retrieve the value from the <input> and trim that
      // value of it's leading/trailing white-space:
      selector = input.value.trim(),
      // we retrieve the element with id=results:
      output = get('#results'),
      // we use the CSS.supports() function to assess whether
      // the browser supports the supplied selector, which
      // we pass to the function in a template string
      // concatenating the selector within the string
      // 'selector(...)' as required by the function:
      result = CSS.supports(`selector(${selector})`),
      // creating an <li>:
      listElement = create('li');

    // if the event-type is 'submit' and the user-entered selector
    // - once trimmed of leading/trailing white-space - is zero-
    // length we return at this point:
    if (evt.type === 'submit' && selector.trim().length === 0) {
      return false;
    }

    // here we add the class of 'supported' or 'unsupported' based on
    // the 'result' variable being exactly equal to (Boolean) true
    // or not:
    listElement.classList.add(result === true ? 'supported' : 'unsupported');

    // here we set the innerHTML of the <li> element to the template string
    // from the defined function (above):
    listElement.innerHTML = templatedResult(selector);

    // we then use Element.prepend() to insert the new <li>
    // as the first child of the 'output' element:
    output.prepend(listElement);

  },
  // here we return the first/only <button> and <form> elements:
  button = get('button'),
  form = get('form');

// and we then bind the verifySelectorCompatibility() function
// to the 'click' event of the <button> and the 'submit' event
// of the <form>:
button.addEventListener('click', verifySelectorCompatibility);
form.addEventListener('submit', verifySelectorCompatibility);
*,
 ::before,
::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

form {
  margin-block: 1em;
  margin-inline: auto;
  width: clamp(30rem, 60vw, 1000px);
}

fieldset {
  display: grid;
  gap: 1em;
  grid-auto-rows: min-content;
  grid-template-columns: repeat(4, 1fr);
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

label {
  display: flex;
  flex-flow: row nowrap;
  gap: 1em;
  grid-column: 1 / -1;
  margin-block: 1em;
  margin-inline: 0.5em;
  padding: 0.25em;
}

label span {
  align-self: center;
}

label span::after {
  content: ': ';
}

label input {
  flex-grow: 1;
  object-fit: cover;
  padding: 0.25em;
}

button {
  grid-column: -2 / span 2;
}

#results li {
  border-block-end-width: 3px;
  border-block-end-style: solid;
  font-family: monospace;
  font-size: 1.5em;
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.supported {
  border-block-end-color: lime;
}

.supported::marker {
  content: '\2713';
}

.unsupported {
  border-block-end-color: red;
}

.unsupported::marker {
  content: '\2717';
}
<form action="#" method="post">
  <fieldset>
    <legend>Does your browser support..?</legend>
    <label>
      <span>Enter a selector to test</span>
      <input type="text" id="testSelector">
    </label>
    <button type="button">Check your browser</button>
  </fieldset>
  <ul id="results"></ul>
</form>

JS Fiddle demo.

References:

David Thomas
  • 249,100
  • 51
  • 377
  • 410
  • In my original question, I was asking for a simple way to create a JS fallback for browsers that do not support a certain selector. This answer is way too elaborate, in my opinion, and it would fit much better as an answer if you would simply have answered with a single script that functions like `if (!CSS.supports('selector(:checked)')) fallbackChecked();`, and then explained what this `CSS.supports` function does. However, even though your answer contains quite a lot of unnecessary bloat that distracts from the solution, I do very much appreciate pointing me towards `CSS.supports`. Thanks! – Joeytje50 Aug 03 '22 at 15:41