I’ve tested all this on IE9, Chrome 19, Safari 5 and Firefox 12. Your mileage will vary for earlier browsers, especially earlier Internet Explorers. If you’re targeting IE < 9 I’d recommend taking a long look at Peter-Paul Koch’s compatibility tables to watch out for potential landmines.

Let’s take a look at the normal order of events when a user triggers the click event on an input element of type="checkbox" (this could happen through a mouse click, a keyboard command, or a touch event; they all trigger the click event):

  1. mousedown/keydown/touchstart
  2. mouseup/keyup/touchend
  3. change (see Special Note for Firefox below)
  4. click

At the end of the process, these four events have fired and the checkbox has toggled its state (from checked to unchecked or vice versa). But what if you want to conditionally prevent the state change? For instance, clicking the checkbox may trigger some kind of validation; if the validation is unsuccessful we don’t want the checkbox to toggle. Returning false from the change event handler has no effect; the default handler for this event can’t be prevented. This means we want to capture the click event.

So now that we’re writing our onclick function, how are we going to check the state of element? Our context is an event handler, which means this is bound to the element which fired the event. (You could also access the element via the target/srcElement properties of the event object, but for this example I’m going to ignore the event object altogether and use this; the results are identical.) The property of the input element that we want is checked; this will be true if the element is checked and false if it’s not.

Okay, all our pieces are in place and we’re ready to write our handler. Should be easy, right?

 <code> var clickHandler = function() { // verify the user is allowed to toggle the checkbox if (validateForm()) { // toggle the checkbox this.checked = !this.checked; } else { // notify user why the checkbox didn't toggle validationNotification(); } // prevent the default action; we already toggled the element return false; } // bind the handler to the element document.forms[0].elements[0].onclick = clickHandler; </code> 

Unfortunately, there are several problems with this function, starting with the detection of the element’s state. Remember the order of events from above? The element actually toggles states before the onclick handler ever fires. In fact, it toggles before the onchange handler even fires (except for Firefox; see Special Note at the end). So by the time your onclick handler has a chance to read this.checked, the element has already toggled to its new state.

Simple fix, right? All we have to do is change this:

this.checked = !this.checked;

to this:

this.checked = this.checked;

Right? No! If you think this looks wrong, there’s a good reason: it won’t work at all. In fact, the entire logical structure of the function above is wrong for what we’re trying to accomplish. Let’s take a look behind the scenes and find out what the browser is actually up to:

  1. mouseup/keyup/touchend event finishes
  2. The browser toggles the state of the target element
  3. The browser fires the change event (again, Firefox is special)
  4. The browser then calls the onclick handler and, based on the return value, decides what to do with the checked state
  5. If the onclick handler returned false, the browser reverts the element back to its pre-change state, regardless of the current state of the element

This means that the checkbox in the above example will never toggle no matter what changes you make to this.checked; the browser will overwrite the changes after the handler has returned. Note that this ONLY happens when the event handler returns false; if you return true (or don’t return anything) your changes to checked will not be overwritten.

This is described in a note in the HTML DOM specification:

Note: During the handling of a click event on an input element with a type attribute that has the value “radio” or “checkbox”, some implementations may change the value of this property before the event is being dispatched in the document. If the default action of the event is canceled, the value of the property may be changed back to its original value. This means that the value of this property during the handling of click events is implementation dependent.

So what does all this mean? The long and the short of it is that in the context of a click handler, the checked property of a checkbox is effectively read-only if the default action is prevented. Further, the state of the checked property inside the click handler is the state that the browser anticipates the property to be if the event’s default handler is called, whether the default handler is called or not. In order to conditionally allow the state toggle, you’ll need to account for this in the return value of your click handler:

 <code> var clickHandler = function() { // verify the user is allowed to toggle the checkbox if (validateForm()) { // checkbox is already toggled; allow this to carry through return true; } else { // notify user why the checkbox didn't toggle validationNotification(); // revert the checkbox to its previous state return false; } } // bind the handler to the element document.forms[0].elements[0].click = clickHandler; </code> 

Special note for Firefox:
Of course, Firefox has to be special. In Firefox, the change event fires after the click event. Returning false from the onclick handler also prevents the change event from firing. In all other browsers I tested, the change event fires regardless of the onclick event handler’s return value. The value of the checked property inside the onclick handler is the same as I describe above.