Why is my jQuery :not() selector not working in CSS?

15,216

Why does the :not() selector work in jQuery but fail in CSS? Shouldn't it work identically in both places since jQuery claims to be "CSS3 Compliant", or is there something I'm missing?

Perhaps it should, but it turns out that it doesn't: jQuery extends the :not() selector such that you can pass any selector to it, no matter how complex it may be, and I suspect that the main reason for this is for parity with the .not() method, which also takes any arbitrarily complex selector and filters accordingly. It does in a way maintain a CSS-like syntax, but it extends from what's defined in the standard.

As another example, this works just fine (I know it's an incredibly ludicrous example compared to what's given in the question, but it's just for illustrative purposes):

/* 
 * Select any section
 * that's neither a child of body with a class
 * nor a child of body having a descendant with a class.
 */
$('section:not(body > [class], body > :has([class]))')

jsFiddle preview

Remember that passing a comma-separated list of selectors to :not() means filtering elements that don't match any of the listed selectors.

Now the :not() pseudo-class in Selectors level 3, on the other hand, is very limited by itself. You can only pass a single simple selector as an argument to :not(). This means you can pass only any one of these at a time:

  • Universal selector (*), optionally with a namespace
  • Type selector (a, div, span, ul, li, etc), optionally with a namespace
  • Attribute selector ([att], [att=val], etc), optionally with a namespace
  • Class selector (.class)
  • ID selector (#id)
  • Pseudo-class (:pseudo-class)

So, here are the differences between jQuery's :not() selector and the current standard's :not() selector:

  1. First and foremost, to answer the question directly: you can't pass a comma-separated selector list.1 For example, while the given selector works in jQuery as demonstrated in the fiddle, it isn't valid CSS:

    /* If it's not in the Α, Β or Γ sectors, it's unassigned */
    #sectors > div:not(.alpha, .beta, .gamma)
    

    Is there a pure CSS workaround for this or will I have to rely on a script?

    Thankfully, in this case, there is. You simply have to chain multiple :not() selectors, one after another, in order to make it valid CSS:

    #sectors > div:not(.alpha):not(.beta):not(.gamma)
    

    It doesn't make the selector that much longer, but the inconsistency and inconvenience remain evident.

    Updated interactive jsFiddle preview

  2. You can't combine simple selectors into compound selectors for use with :not(). This works in jQuery, but is invalid CSS:

    /* Do not find divs that have all three classes together */
    #foo > div:not(.foo.bar.baz)
    

    You'll need to split it up into multiple negations (not just chain them!) to make it valid CSS:

    #foo > div:not(.foo), #foo > div:not(.bar), #foo > div:not(.baz)
    

    As you can see, this is even more inconvenient than point 1.

  3. You can't use combinators. This works in jQuery, but not CSS:

    /* 
     * Grab everything that is neither #foo itself nor within #foo.
     * Notice the descendant combinator (the space) between #foo and *.
     */
    :not(#foo, #foo *)
    

    This is a particularly nasty case, primarily because it has no proper workaround. There are some loose workarounds (1 and 2), but they almost always depend on the HTML structure and are therefore very limited in utility.

  4. In a browser that implements querySelectorAll() and the :not() selector, using :not() in a selector string in a way that makes it a valid CSS selector will cause the method to return results directly, instead of falling back to Sizzle (jQuery's selector engine which implements the :not() extension). If you're a stickler for performance, this is a positively minuscule bonus you'll definitely salivate over.

The good news is that Selectors 4 enhances the :not() selector to allow a comma-separated list of complex selectors. A complex selector is simply either a lone simple or compound selector, or an entire chain of compound selectors separated by combinators. In short, everything you see above.

This means that the jQuery examples above will become valid level 4 selectors, which will make the pseudo-class much, much more useful when CSS implementations begin supporting it in the coming years.


1 Although this article says that you can pass a comma-separated list of selectors to :not() in Firefox 3, you're not supposed to be able to. If it works in Firefox 3 as that article claims, then it's because a bug in Firefox 3 for which I can't find the ticket anymore, but it shouldn't work until future browsers implement future standards. Seeing how often that article is cited to date, I've left a comment to this effect, but seeing also how old the article is and how infrequently the site is being updated, I'm really not counting on the author coming back to fix it.

Share:
15,216
BoltClock
Author by

BoltClock

My pronouns are they/them. Learn more. Curious about my profile picture? Check out my Flash game on Newgrounds! I'm a software developer who relishes authoring HTML and CSS on the web and hacking C# on their self-built Windows PC. I adore Jesus and the colors green, purple and teal, and I often catch myself thinking in numbers and code on a whim. I was also a Microsoft MVP from 2017 through 2020. My areas of specialty are in web design — I've known HTML and CSS for more than a decade now! — and Windows app development with C# and XAML. Other languages I've worked with include (from most to least experience) PHP, SQL, ActionScript, Swift, Objective-C, Perl, Python, and Java. Roles on this site From the tags I participate in (below) as well as my bio (above), it's easy to tell that I'm interested in a number of languages and topics. However, you'll most often find me patrolling, curating and moderating the following tags: htmlcsscss-selectorsinternet-explorermicrosoft-edge As a moderator, I believe I am more efficient than ever with post revisions and other housekeeping duties, as I continue with my question-answering business. While I actively add fresh content to the site, I'm also helping out with cleaning and polishing our currently-existing posts. "Dust tends to settle", and all that jazz. Achievements Singapore's top user by reputation and first community moderator ♦ on Stack Overflow! 1st user to earn the bronze, silver and gold css-selectors badges! 4th user to earn the gold css badge! 9th user to earn the gold html badge! 45th user to earn the Legendary badge! 54th user to reach the 100,000-reputation milestone!

Updated on June 06, 2022

Comments

  • BoltClock
    BoltClock almost 2 years

    I have this layout:

    <div id="sectors">
        <h1>Sectors</h1>
        <div id="s7-1103" class="alpha"></div>
        <div id="s8-1104" class="alpha"></div>
        <div id="s1-7605" class="beta"></div>
        <div id="s0-7479"></div>
        <div id="s2-6528" class="gamma"></div>
        <div id="s0-4444"></div>
    </div>
    

    With these CSS rules:

    #sectors {
        width: 584px;
        background-color: #ffd;
        margin: 1.5em;
        border: 4px dashed #000;
        padding: 16px;
        overflow: auto;
    }
    
    #sectors > h1 {
        font-size: 2em;
        font-weight: bold;
        text-align: center;
    }
    
    #sectors > div {
        float: left;
        position: relative;
        width: 180px;
        height: 240px;
        margin: 16px 0 0 16px;
        border-style: solid;
        border-width: 2px;
    }
    
    #sectors > div::after {
        display: block;
        position: absolute;
        width: 100%;
        bottom: 0;
        font-weight: bold;
        text-align: center;
        text-transform: capitalize;
        background-color: rgba(255, 255, 255, 0.8);
        border-top: 2px solid;
        content: attr(id) ' - ' attr(class);
    }
    
    #sectors > div:nth-of-type(3n+1) {
        margin-left: 0;
    }
    
    #sectors > div.alpha { color: #b00; background-color: #ffe0d9; }
    #sectors > div.beta  { color: #05b; background-color: #c0edff; }
    #sectors > div.gamma { color: #362; background-color: #d4f6c3; }
    

    I use jQuery to add the unassigned class to sectors that don't otherwise have one of the classes alpha, beta or gamma:

    $('#sectors > div:not(.alpha, .beta, .gamma)').addClass('unassigned');
    

    Then I apply some different rules to that class:

    #sectors > div.unassigned {
        color: #808080;
        background-color: #e9e9e9;
        opacity: 0.5;
    }
    
    #sectors > div.unassigned::after {
        content: attr(id) ' - Unassigned';
    }
    
    #sectors > div.unassigned:hover {
        opacity: 1.0;
    }
    

    And everything works flawlessly in modern browsers.

    Interactive jsFiddle preview

    But seeing as the :not() selector in jQuery is based on :not() in CSS3, I was thinking I could move it directly into my stylesheet so I wouldn't have to rely on adding an extra class using jQuery. Besides, I'm not really interested in supporting older versions of IE, and other browsers have excellent support for the :not() selector.

    So I try changing the .unassigned portion above to this (knowing I will only have sectors Α, Β and Γ in my layout):

    #sectors > div:not(.alpha, .beta, .gamma) {
        color: #808080;
        background-color: #e9e9e9;
        opacity: 0.5;
    }
    
    #sectors > div:not(.alpha, .beta, .gamma)::after {
        content: attr(id) ' - Unassigned';
    }
    
    #sectors > div:not(.alpha, .beta, .gamma):hover {
        opacity: 1.0;
    }
    

    But as soon as I do this it stops working — in all browsers! My unassigned sectors aren't grayed out, faded out or labeled 'Unassigned' anymore.

    Updated but not so interactive jsFiddle preview

    Why does the :not() selector work in jQuery but fail in CSS? Shouldn't it work identically in both places since jQuery claims to be "CSS3 Compliant", or is there something I'm missing?

    Is there a pure CSS workaround for this or will I have to rely on a script?