9/06/2010

Reinventing a Drop Down with CSS and jQuery

For me, standard HTML Select element is pretty much annoying. It's ugly. It can't be styled properly in Internet Explorer. And it can't contain nothing but simple text. That is the reason why I needed to reinvent Drop Down element. This tutorial shows how to do that (easily, believe it or not).

View demo Download source code

There is an update at the end of this tutorial and the second demo that shows the results of this updated code.

Simple structure

Let me explain HTML structure that will be used here. In this example we will use a short list of 8 countries. List is created using Definition List (DL) element. Why this element? It is similar to unordered/ordered list - the only difference is that DL consists of two elements: DT (term) and DD (definition). This makes it perfect candidate for Drop Down element. Element DT can be used to show collapsed state with currently selected option while DD can show all the available options in nested UL. Here is the sample structure:



In order to make Drop Down functional we have to add several important CSS styles. First of all we have to reset margins and paddings for DD, DT and UL. DD will be positioned relatively, so that nested UL can be absolutely positioned. As I mentioned earlier, in collapsed state, only DT will be visible. It contains link with span inside it and two elements make sliding doors, a technique that is often used for creating buttons and tabs. Styling UL is simple, it will be positioned 2px below DT and initially hidden.

/* General dropdown styles */     

.dropdown dd, .dropdown dt, .dropdown ul { margin:0px; padding:0px; }
.dropdown dd { position:relative; }
/* DT styles for sliding doors */
.dropdown dt a {background:#e4dfcb url(arrow.png) no-repeat scroll right center;
display:block; padding-right:20px; border:1px solid #d4ca9a; width:150px;}
.dropdown dt a span {cursor:pointer; display:block; padding:5px;}
/* UL styles */
.dropdown dd ul { background:#e4dfcb none repeat scroll 0 0; display:none;
list-style:none; padding:5px 0px; position:absolute;
left:0px; top:2px; width:auto; min-width:170px;}
.dropdown span.value { display:none;}
.dropdown dd ul li a { padding:5px; display:block;}

After adding some colors, borders and hover effects (check out source code on demo page), the result so far will be something that really looks like Drop Down.

Let's make it work

It's time to involve jQuery. Each click on DT (actually link inside DT) will toggle nested UL.

$(".dropdown dt a").click(function() {

$(".dropdown dd ul").toggle();
});

This was the simplest part. Now let's simulate other features of Select element. When you choose an option from the list (UL) it become selected option and is shown inside the link in DT. The function below replaces the HTML of currently selected item with the inner HTML of clicked link. At the end, it hides nested UL.

$(".dropdown dd ul li a").click(function() {

var text = $(this).html();
$(".dropdown dt a span").html(text);
$(".dropdown dd ul").hide();
});

This looks more like Drop Down, but there are still some things that need to be done. First, once you reveal nested UL by clicking on Drop Down it remains visible. And that's a bug. Although there can be better solutions, I've came up with this one:

$(document).bind('click', function(e) {

var $clicked = $(e.target);
if (! $clicked.parents().hasClass("dropdown"))
$(".dropdown dd ul").hide();
});

The function above checks each click on a page and if click occurred on some elements outside the dropdown it hides nested UL. Now this looks like regular Select element.

What about selected value?

Although it looks fine it will surely become a headache for developers. Select element has "value" attribute where you can store some data then needs to be sent to the server. This attribute is usually populated by ID's of records stored in the database. In our example with list of countries, it is more likely that some country ID will be needed for any serious processing on the server. So how to store these values and how to get selected one?

Let's modify HTML structure from the beginning of this tutorial and add element inside links in UL. Each have class "value" and real value inside it.



It would be strange to have codes next to country names in the list, so let's hide these spans.

.dropdown span.value { display:none;}

And that is the only change we'll need to make in order to make this Drop Down fully functional. Take one more look at click handler for links in UL - it replaces inner HTML of link in DT with inner HTML of clicked link. This means tha tlink in DT element will have with value included.

var text = $(this).html();

$(".dropdown dt a span").html(text);

It's easy now to get selected value from our Drop Down. You can create a function like the one below to extract selected value. As the matter of fact, the very same function extracts selected value in the demo and shows this value under the Drop Down.

function getSelectedValue(id) {

return $("#" + id).find("dt a span.value").html();
}

The code works in all major browsers: Firefox, Safari, Google Chrome, Opera(9.64), Internet Explorer 7 and 8. It works even in IE6, althoug it needs some polishing :)

Update (29.07.2009): Creating DropDown from SELECT

Since many of you pointed out the issue with accessiblity (and I agree with that) I made another example that creates DropDown dynamically from SELECT element and binds click event to it. It still doesn't have up/down key navigation, though.

Instead of having hard-coded DL, we will have SELECT element with all the items here:



We will then use jQuery to dynamically create list from SELECT:

function createDropDown(){

var source = $("#source");
var selected = source.find("option[selected]"); // get selected
var options = $("option", source); // get all elements
// create
and
with selected value inside it
$("body").append('
')
$("#target").append('
' + selected.text() +
'' + selected.val() +
'
')
$("#target").append('
    ')
    // iterate through all the elements and create UL
    options.each(function(){
    $("#target dd ul").append('
  • ' +
    $(this).text() + '' +
    $(this).val() + '
  • ');
    });
    }

    Let me briefly explain the code here (you can check the entire source code in the second demo). It first reads all the elements and selected element as well. Next, it creates DL and DT with selected item inside it. After DL is being created, the code iterates throug collection and create UL with list items inside it.

    In order to fully bind this DropDown to SELECT element we need to refresh SELECT each time new selection is made in DropDown. We have to modify click event handler for links in UL. It will get the selected item from the DL and assign a value from to SELECT element.

    $(".dropdown dd ul li a").click(function() {
    
    var text = $(this).html();
    $(".dropdown dt a").html(text);
    $(".dropdown dd ul").hide();
    var source = $("#source");
    source.val($(this).find("span.value").html())
    });

    That should fix the issue. Thanks everyone for pointing out this.

    View SECOND demo

    Wasn't that simple?

    I think that reinvention of Drop Down wasn't that hard, although it might seems as is it. There are several features that need to be implemented in order to have a credible simulation, but each one of those is simple and straightforward. Have you ever had a need for reinventing Drop Down? What are your experiences? In any case, let me know your thoughts!

    No comments: