CSS Checkboxes and (More) Accessible Drop-downs

Here's a pure CSS solution to the challenge of styling input check-boxes. The check-boxes also act as a trigger for displaying hidden content via the :checked psuedo-class. You might use this mechanism as a means for putting together drop-downs and accordion-menus, although the technique is most applicable to HTML forms. You'll find some 508/WCAG considerations here to make this UI as accessibility compliant as possible.

Accessibility Disclaimer

I'm not an accessibility auditor, so don't take it for granted that what you find here is fully 508/WCAG compliant. I leave these kinds of determinations to proper auditors. Nonetheless, standards compliance is an issue I take seriously, and I've made every attempt to keep this code as accessible as possible. Use at your own risk. If accessibility isn't a thing for you, be damned with the consequences and have at it!

Background - UI Goals

A client has some accordion/drop-down like menus on their site they need revamped. One of the issues they'd like to address is making the accordion headers more clickable--they'd like a little more context aimed at drawing users towards engagement. Stakeholders asked that we put some check-boxes next to the headers. This may not be ideal in semantic sense (really, form elements are best suited to forms), but it's what the client wants--so, off to the drawing board we go. An added challenge is to keep the UI as accessible as possible: I need to shy away from "display: none;", "visibility: hidden;" and JavaScript implementations. We want hidden content to be hidden visually, but accessible to anyone who comes to the page with special needs. We also want an appealing and engaging UI.

Development - A Check-box Challenge

Before we get to the nitty-gritty, let's look at the final product--go ahead and play with the check-boxes below:

The essence of what's going here is this: I'm obviously hiding some drop-down text, but I'm also hiding all of the actual <input> check-boxes. Since input elements aren't easily styled (as far as I know), I've had to find a different way of styling the check-boxes. Luckly, the <label> element allows me to pull this off in a couple of ways:

  1. I can create a :before psuedo-element relative to the label element which can be used as a proxy for the actual check-box.
  2. I can tie the label to the check-box input--such that when the label is clicked it also checks the real (hidden) check-box. I can then also use the :checked psuedo-class to toggle styling for my proxy.

The latter of these really deserves the most attention, since it ensures that the above example actually functions; I'll assume you know how to style out a :before psuedo-element (you can check my CSS in the final Fiddle if you like, but either way that's not the point of this post). With that understood, let's look a little closer at how we pull this together.

Getting Your HTML in Order

The first step is really to make sure our HTML will actually let us do what we want. There are two ways to tie a label element to an input element: we could wrap the input inside the label, or we could place the elements side by side and make use of the for and idatrributes. If you wrap the input inside the label, I have a feeling the only way you can pull this off is to use JavaScript. Rather, we'll want the input element to come immediately before the label element; we'll give the input a unique id attribute and then match that value to a for attribute on the label. A very simplified version of your HTML might look, then, like this:  

<div class="list-wrap">
  <fieldset>
    <legend>Make a Selection</legend>
    <div class="parent"><input id="drop-one" type="checkbox"><label for="drop-one">Selection One</label>
      <div class="child">
        <p>This is some hidden text</p>
      </div>
    </div>
  </fieldset>
</div>

We keep the input element before the label so that it's easier to target the :before psuedo-element we use as a proxy for our checkbox; that element will need to have two styles: checked and unchecked. By placing the input element before the label, we can use the CSS :checked psuedo-class in combination with the + selector to change the styling of the :before psuedo-element once the label is clicked. Since our child (hidden) div is also downstream from the input element, we can use the same + selector to toggle visibility. Here's how you might accomplish all this with some simplified SCSS (a lot of styling purposely left out):  

div.parent {
  //HIDE THE CHILD DIV BY DEFAULT 
  div.child {
    margin: 0px 0px 0px 35px;
    //DEBATABLE WHETHER OPACITY 0 IS ACCESSIBLE, SO SOMETHING CLOSE TO ZERO JUST IN CASE    
    opacity: 0.001;
    height: 0px;
    width: 0px;
    //AND FLATTEN ELEMENTS INSIDE CHILD DIV
    p, a {
      display: inline-block;
      height: 0px;
      width: 0px;
      margin: -1px;
    }
  }
}

label {
  //SO WE CAN POSITION THE PSUEDO-ELEMENT RELATIVE TO LABEL 
  position: relative; 
  display: inline-block; 
  cursor: pointer;
  //CREATE SOME MARGIN FOR PSUEDO-ELEMENT
  margin-left: 26px;
  &:before {
    content: '+';
    cursor: pointer;
    position: absolute;
    width: 18px;
    height: 18px;
    left: -20px;
    top: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}

//IF ANY INPUT WITH ID CONTAINING "drop" IS CHECKED, CHANGE THINGS UP
input[id*="drop"]:checked + label {
//MAYBE DIFFERENT LABEL STYLING?
  &:before {
    //PUT SOME DIFFERENT PSUEDO-ELEMENT STYLING HERE
  }
  //TOGGLE VISIBILITY OF HIDDEN DIV
  + div.child {
    margin: 10px 0px 0px 35px;
    opacity: 1;
    height: auto;
    width: auto;
    p, a {
      display: inline;
      //INLINE ITEMS CAN'T HAVE HEIGHT/WIDTH, SO NO NEED TO FIX THOSE
    }
    p {
      margin: 0px 0px 10px 0px;
    }
  }
}

Hopefully that isn't too hard to follow. I've left out most of the actual styling. The important part is, again, the :checked psuedo-class. Let's see if we can get our heads around that. The line reads: input[id*="drop"]:checked + label. This means, target the label element that immediately follows any checked input with an id attribute containing the word "drop". In my case, I've actually got several label elements, and I need to target them all. Each one will have a unique id attribute, but they will all contain the word drop. It's this mechanism that also allows us to style the :before psuedo-element as well. The + selector is what requires us to have the input element before the label element (and not inside it); again, if the input were wrapped inside the label, it would become a child element and we'd have no way of targeting the label once the input was checked. Likewise, we'd have a hard time targeting the hidden child div. The logic for targeting the child div is the same as for the label; since the child div immediately follows the label, targeting it in standard CSS would look like this: input[id*="drop"]:checked + label + div.child.

The rest is really just styling your label and checkbox proxies.

Notes on Accessibility

The check-box input is hidden by placing it off screen; it's not really gone--it's just not visible. This is a common technique for accessibly hidden content:

input { 
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
}

The child div is hidden with opacity, height and width. Opacity is, however, debatable as a method for hiding content accessibly. I've seen sources that say it's okay, and others that suggest not. It seems, though, that a good compromise is to go with opacity: 0.001 if in doubt. I definitely want to make use of opacity in my drop downs if only because I have a transition effect at play--allowing the text to "fade" in when a selection is made. I also opted to flatten subordinate <p> and <a> elements beneath the child div. To do this, these elements need to have CSS block properties. If you make them true blocks, however, they'll break onto their own lines; this will make the transition goofy when they slide open. To avoid this, inline-block is the better option.

You could hide the child div and its contents using the same method I've used with the input element. I don't think that would preclude you from still leveraging a transition. 

Lastly, since these are technically form elements, they should be wrapped inside a <fieldset> element--and, that element should rightly have a legend. This is probably the kind of thing my clients would push back on, but the legend can be positioned accessibly off-screen and the fieldset element doesn't actually have to have a visible border.

The Real Deal

Enough blabbering. Here it is, the whole enchilada, HTML, SCSS and all: