Grow your CSS skills. Land your dream job.

More than one way… (delegate edition)

Published by Chris Coyier

There was a question in the forums about affecting non-hovered items. The effect they were after is that they had an unordered list of items and when they were rolled over, they would all dim (lower opacity) except the one hovered.

This can be done with CSS, using pseduo-selectors.

ul li:not(:hover) { opacity: 0.5; }

However we know that pseudo-selectors don't have very good cross-browser support. And for that matter, opacity doesn't either. jQuery is pretty good at mitigating cross-browser problems, so l thought I might give that a spin. In attempting it, I had a nice little learning journey.

My first thought is that I needed to write a selector to select all list items except the one currently being hovered over. I had this come up recently for another reason, and I made a snippet for it: Excluding this from the selector. In this example:

$("ul li").not(this).css("opacity", "0.5");

We just need to wrap that in some kind of hover function. The most obvious:

$("li").hover(function() {
  $("li").not(this).css("opacity", 0.5);
}, function() {
  $("li").not(this).css("opacity", 1);
});

But I've been being taught that binding events like this is inefficient, since 1) it requires one event handler for every single element and 2) new elements appended to the page after this code runs will need to be re-bound. (Not to mention, you would definitely want to cache that selector above var $listItems = $("li");).

So I thought I'd go right for the new delegate function which we have mentioned here on CSS-Tricks a few times now. This is awesome because it solves both the issues mentioned above. This was my first (utterly broken) attempt.

$("ul").delegate("li", "hover", function() {
    $("ul li").not(this).css("opacity", "0.5");
}, function() {
    $("ul li").not(this).css("opacity", "1");
});

Don't try that at home, it's not going to work. Why not? James Padolsey reminded me that "hover" isn't an event. It's a jQuery function, but not a real event. You can use it with delegate, but not with the same syntax like I was attempting. Delegate is expecting just the three parameters: element, event, and function, not four parameters like I was passing it (assuming it would know the last function was supposed to be a callback/mouseleave).

Then the next most obvious transformation becomes this:

$("ul").delegate("li", "mouseenter", function() {
    $("ul li").not(this).css("opacity", "0.5");
}).delegate("li", "mouseleave", function() {
    $("ul li").not(this).css("opacity", "1");
});

That uses two delegate functions, this time with real events, to get the job done. This is fine, but we can make it a bit more efficient by mapping both events to a single delegate and then just testing to see what type of event was fired. David Link had this idea:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    if (e.type == 'mouseover') {
      $("ul li").not(this).css("opacity", "0.5");
    } else {
      $("ul li").not(this).css("opacity", "1");
    }
});

Which James Padolsey had an even cleaner version:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    $("ul li").not(this).css("opacity", e.type == 'mouseover' ? 0.5 : 1);
});

jQuery's live() function is also a good choice here, but has some quirks as well. It turns out you can pass "hover" to live, but you can still only provide a single function. The function will then fire on both mouseenter and mouseleave events, and you'll have to do event.type testing (like above) to figure out which it was and behave accordingly. Thanks to Paul Irish and Jeffrey Way for that one.

And remember that CSS selector from the very top? We can use that right in jQuery too:

$("ul li:not(:hover)").css("opacity", "0.5");

Quite the journey eh? Like all things web, always more than one way to skin the cat.

Comments

  1. Nice post – I just wanted to mention that support for hover has actually been added for delegate (I believe in 1.4.1).

    It essentially just maps both mouseover and mouseout to the callback function (so you still have to differentiate between the event types, as in your examples). Here’s a quick example: http://jsbin.com/idibu/edit

  2. Permalink to comment#

    Do you write all of your articles in real-time like this, Chris?

  3. Oops… just tried it with 1.4.1 and it failed. Must’ve been a 1.4.2 addition.

  4. I hope same can be done more easily and without delegate like this

    $("ul li").hover(function(){
      $(this).css("opacity", "0.5").siblings().css("opacity", "1");
    });
    • Nice use of .siblings(). But I think that would have the issue of requiring as many event handlers as there are list items which is what delegate was trying to avoid.

      But I also heard murmors that the hover function automatically uses live? Is that true?

    • Hover doesn’t automatically use live, so as you say, it would create an event handler (technically two) for each list item.

  5. Has mouseover jumped the shark now that iPhone/iPad don’t support it? What’s the alternative?

    For example, I wanted to create a “spoiler” class that would display on mouseover. Is there an solution that works with both mouse-based interfaces and touch interfaces?

  6. Michał Czernow
    Permalink to comment#

    Hm, quick not tested CSS thought:
    li:hover ~ li { ... }
    Do you think it might work?

    • Michał Czernow
      Permalink to comment#

      Won’t work; selects only siblings after, not before left element :(.

  7. Permalink to comment#

    ul:hover li { opacity:0.5; }
    ul:hover li:hover { opacity:1; }

    This would do exactly what you said… all list items would dim 50% except that list item you’re currently hovering… No fancy pseudo selectors or JS needed.

    • Well perhaps I didn’t make it very clear, but the point is also that the list items are all at full opacity when none of them are moused over.

    • José Pedro
      Permalink to comment#

      But having “ul:hover” instead of just “ul” would make it half opacity only when the list is hovered, thus fulfilling the point.

    • Blue
      Permalink to comment#

      It still fails with browser support because of the opacity, and that some broswers don’t hovers on non-anchor elements. Hence the jQuery for browser support way of doing this.

    • Permalink to comment#

      This would only make the other list items transparent when you’re hoving over the UL.

      Of course, it doesn’t work in IE6 (can’t detect hover on lists or list items). but for opacity, we can just use the IE filter…

    • Michał Czernow
      Permalink to comment#

      After my failure, I came with same idea (only difference is: ul li:hover…). So, totaly agree with You, Sean. Nice thought.

    • The css is achieving what is required apart from browser issues it is a quick fix

    • Michał Czernow
      Permalink to comment#

      Opacity can be managed with conditional comments, ms-filters and csshover.htc for not-anchor hovers :). A bit more work, but a bit less kB and in some browsers less http requests. Should work from IE6 to IE8.

    • Dave
      Permalink to comment#

      I was going to post the same snippet, seems like a much less hacky approach. Chris, me thinks you may have over thought this problem.

      Maybe you need all that JS for IE6, but I don’t recognize/support that “browser” anymore.

  8. dcp3450
    Permalink to comment#

    Thanks for the write up. Glad my question spawned a great article. :)

  9. Wait one second Chris, there’s something that you haven’t quite thought through here. You absolutely must use $(this).siblings() (like in abhishek’s example, but preferably with delegate) like this:

    $("ul").delegate("li", "mouseover mouseout", function(e){
        $(this).siblings().css("opacity", e.type == 'mouseover' ? 0.5 : 1);
    });​

    Why?

    Well, the reason is that all of your examples fail when there are multiple lists on the page, since instead of operating relative to the li or ul in question, they operate on every single list on the page!

    View these examples, and you’ll understand what I mean: your version (oops) and fixed version (yay).

    • Sure, that’s a cool enhancement… The whole concept was super abstract to begin with and we didn’t really define the desired behavior with multiple lists so I wasn’t that concerned about it. I could see cases where you could desire either result.

    • Chris, I’ve found that in general, using context-aware, relative selectors, starting with $(this) when possible, makes for substantially more maintainable code… and I know it can be argued either way, but it seems like the most common use-case for a behavior like this would be per-list, and not global.

      Still, despite this particular issue’s subjectivity, one of my biggest concerns these days continues to be: how do those of us writing articles present concepts and examples to people in a way that not only gets them thinking about that concept, but also thinking about the implementation as well.

      I’ve been mulling over an article about this for a while now, perhaps I should get writing.. don’t mind me! :)

  10. Kyle Kinnaman
    Permalink to comment#

    I think we’re sharing a brain. I’ve spent the last few days cursing about mouse events in trying to get live() and hover to play nicely with multiple jsonp objects. I finally gave up and made it a good old click event.

  11. Nice post, there really are about a million ways to do anything you want to do in web development, and a good bit of exploration to find the most efficient is awesome.

    However, I do feel the need to correct one thing you said here:

    that “hover” isn’t an event. It’s a jQuery function, but not a real event, and will not work with delegate.

    You are correct in the first part, that hover is a jQuery function and not an event, but you are incorrect on the second part. Hover does indeed work with delegate (and live as well). You just have to use it a bit differently.

    In fact, the hover function is specifically used in the jQuery API documentation of delegate(): http://api.jquery.com/delegate/

  12. In a scenario like this, I would opt for progressive enhancement: just implement it using CSS and have no mercy for browsers that don’t support it.

    We should make it clear that some browsers suck by reducing their experience. This will push vendors to improve and users to upgrade.

    Instead, we are lengthening the problem once again by using JS hacks to accomplish something that should not be done in JS.

    No disrespect for the article and the cleverness behind the solution. I also understand how clients can push you to a solution like this.

  13. dcp3450
    Permalink to comment#

    i used


    $("ul").delegate("li", "mouseover mouseout", function(e) {
    $("ul li").not(this).css("opacity", e.type == 'mouseover' ? 0.5 : 1);
    });

    and IE (of course) messes with the alignment of my menu items. It also causes the text to look jagged.

  14. Thats awesome man both mousehover and hover function i am just learning CSS

  15. Permalink to comment#

    a slight enhancement for this, you might add a webkit transition for this too (for browsers that support it)

    ul {-webkit-transition: all 0.2s ease-out;}

  16. mike
    Permalink to comment#

    You guys know a ton more about javascript than I do.

    I am finding that javascript is a super useful thing to know so I am just starting out.

    I use wordpress as a CMS and I have been trying to implement some javascript into my theme (built off starkers) and when following tuts online as to how to implement JS into my site I find that I just can’t get it to work.

    This looks like a really cool technique and I would like to try it out. Where to I place all of the code??? I have no idea where to start or how to make this work.

  17. This is a really interesting idea, never thought about using an effect like this. But would look really nice i think!

    jQuery is great but i feel for this kind of effect is a bit overkill, browser support is ever increasing for this kind of thing and with the release of IE9 soon i think IE6 will be dead and buried (Hopefully anyway!)

    Personally I would stick to the CSS coding as mentioned, just did a quick test with the code Sean posted:

    ul:hover li { opacity:0.5; }
    ul:hover li:hover { opacity:1; }

    and it works really well. If your using a browser that doesn’t support opacity then you don’t see the effect but it’s going to harm your design that much in my opinion… Just enhance it for those using decent browsers!

    Thanks for the idea though and and the useful replies from people!

  18. This is a very informative and different way to add opacity to an element.

  19. It’s an interesting article, but i still don’t get why would you do that instead of just doing the way mentioned above:

    ul:hover li { opacity:0.5; }
    ul:hover li:hover { opacity:1; }

    Maybe with the addiction of JS to add classes to ul and li instead of using pseudo :hovers, but enough.

    Is like you are just trying to poke your right ear with your left hand from behind your head! Too much to solve too little. KISS. :]

    • This has been mentioned 3-4 times now… just to be clear, this doesn’t work in the way it was ask for it to work. This would make ALL the list items transparent until they are rolled over. That’s not the goal. The goal is to have them fully opaque until they are rolled over, then to give 50% transparency to the OTHER ones. Check out the first line of CSS from this article, that does the trick.

      Also, the point here was a theoretical/abstract journey in learning a new concept and better ways to do something, not a super-duper-practical real-world example.

    • Permalink to comment#

      It’s certainly great to explore thing, not bashing here.

      Just wanted to point out that it doesn’t make all the items transparent by default. only on rollover of the list, hence the ul:hover li

    • Oh yeah, correct, my bad. Still, the first line of CSS in this article does the same in one line with what I would think to be about the same level of browser compatibility.

    • But if you just add JS for adding the classes, the browser compatibility will increase like… a lot of %s more ’cause you wouldn’t use pseudo :not.

      That’s how I would do if I had to. XD

  20. If it was necessary that only the li which the mouse is over remain in visible, then:
    ul:hover li {
    opacity: 0;
    }
    ul:hover li:hover {
    opacity: 1;
    }

    obviously not supported on IE6 and no animation (except -webkit-transitions, of course) but that does the job…

    Sorry, probably i missed the target

  21. Permalink to comment#

    Great write up, this is a really nice way to get this effect. Can’t wait to try it.

    Thanks.

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".