Thursday, April 25, 2013

A Simple Slider Toggle

rendered slider toggle One of the design elements we've see in native apps on mobile devices is a "slider toggle". This design element is a 2-position toggle that looks a bit like a button set in a channel. In the real world, it can either be horizontally-aligned or vertically-aligned, and they basically look like the images shown here. Jump to Demo

[Note 2013-04-28: I updated this entry to include the basic HTML/CSS.]

[Note 2013-05-16: I updated this entry to address hiding the checkbox and to handle IE prior to 9.]

One of the reasons we see them in a lot of native mobile apps is that they're relatively easy to do, but if you're using HTML and CSS3, they're a little more difficult, but not terribly so. In fact, I put the simple prototype seen here together in about an hour. You can, of course, get all the code at the prototype link, and the prototype is considerably more robust than this entry, but here's the basic concept behind it.

First, a toggle slider s a 2-state, modifiable element - which means we need to use a checkbox - if we used a radio button we would need two to represent the two states. If we have a checkbox, we need a label for accessibility, but that's fairly straight-forward - there's no magic here.

Since we're going to show a different interface than the usual interface for the checkbox, you'll want to hide the checkbox. In order to maintain our accessibility, you definitely don't want to use display:none - which hides something from everything - even screen readers. You also don't really want to move it, because then it loses context given by proximity. Instead, use the clip property - and be careful to use the IE syntax if you're supporting versions prior to 9.

HTML <label for="my-cbx">foo</label>
<input id="my-cbx" type="checkbox" value="on"/>

CSS input[type="checkbox"] {
  position:absolute !important;
  clip:rect(1px 1px 1px 1px);
  clip:rect(1px, 1px, 1px, 1px);
}

Ok, so we have the basic interface set - a checkbox. Now, we need to create the button and the inset channel to set it in. The button is pretty easy - it's just a circle, and a circle is just a square with a radius. So, create a span that is the same size in height as width - I picked 1em, but you could make a larger button. Next, give it a one pixel solid border with a relative radius - one that's 70% of the height will make a nice circle. Since I'm using simple measurements (1em), the radius is .7 em. If you increase one measurement, just remember to increase the other measurements proportionally, e.g. if you make your button 2x2, make the border radius 1.4.

It's important that the button be placed as the next sibling relative to the checkbox - you'll see why when we get to the later CSS. Also, to give my button seem more like a button, I add a linear gradient and set the cursor to pointer. I also have to set it to display as an inline block so the width and height work correctly.

HTML <label for="my-cbx">foo</label>
<input id="my-cbx" type="checkbox" value="on"/>
<span class="toggle-button"> </span>

CSS .toggle-button {
  background-image: linear-gradient(to bottom, rgb(255, 255, 255) 0%, rgb(153, 153, 153) 100%);
  border: 1px solid rgb(0, 0, 0);
  border-radius: 0.7em 0.7em 0.7em 0.7em;
  cursor: pointer;
  display: inline-block;
  height: 1em;
  position: relative;
  width: 1em;
}

Next we'll create the channel - which is also pretty easy. Make it slightly wider (or taller) than twice the width (or height) of your button. Since my button is in 1x1 em, my channel (container) is 2.3 ems; however, 2.2 ems may look better to you and provides easier scaling. Make sure that the display is inline block so the height and width work correctly and that the border radius on the channel is close to (or the same as) the border radius on the button. By the way, the easiest way to adjust the orientation is to add a class (e.g. vertical) and adjust the height and width in that class (e.g. height:2.2em; width:1.1em) - just remember that if you increase the scale of the button and the channel you also increase the scale in CSS rule that handles the other orientation. One last touch - adding an inset shadow gives a little indication of depth to the channel - you might also be able to get the look you want by using an inset or groove, but it never looked right to me.

HTML <label for="my-cbx">foo</label>
<span class="toggle-container">
  <input id="my-cbx" type="checkbox" value="on"/>
  <span class="toggle-button"> </span>
</span>
CSS .toggle-button {
  background-image: linear-gradient(to bottom, rgb(255, 255, 255) 0%, rgb(153, 153, 153) 100%);
  border: 1px solid rgb(0, 0, 0);
  border-radius: 0.7em 0.7em 0.7em 0.7em;
  cursor: pointer;
  display: inline-block;
  height: 0.9em;
  position: relative;
  width: 0.9em;
}
.toggle-container {
  border: 1px solid rgb(0, 0, 0);
  border-radius: 0.7em 0.7em 0.7em 0.7em;
  box-shadow: 0 0 0.1em 0.1em rgb(187, 187, 187) inset;
  cursor: pointer;
  display: inline-block;

  height: 1em;
  margin: 0.1em;
  width: 2.3em;
}

Now that you have the button and channel created, it's time for the "magic". Be sure that you have the position of your button set to "relative", and add the pseudo-class "checked" to the checkbox rule so when it's checked, the left value of the button is set to 50% and the top value is 0 (i.e. left:50%; top:0) - be sure that your vertical class is the inverse of that rule (i.e. left:0; top:50%;). The easiest way to achieve this is to place the "button" directly after the checkbox (i.e. as its next sibling) and use the next sibling selector (i.e. +). As an example, my rule is .toggle-container input[type=checkbox]:checked + .toggle-button {left:50%; top:0;} - but you can modify it so that it changes other properties (e.g. background-color or border-color) when selected as well. I also add a focus psuedo-class to the checkbox so that when we're modifying it the button 'glows'.

HTML <label for="my-cbx">foo</label>
<span class="toggle-container">
  <input id="my-cbx" type="checkbox" value="on"/>
  <span class="toggle-button"> </span>
</span>
CSS .toggle-button {
  background-image: linear-gradient(to bottom, rgb(255, 255, 255) 0%, rgb(153, 153, 153) 100%);
  border: 1px solid rgb(0, 0, 0);
  border-radius: 0.7em 0.7em 0.7em 0.7em;
  cursor: pointer;
  display: inline-block;
  height: 0.9em;
  position: relative;
  width: 0.9em;
}
.toggle-container {
  border: 1px solid rgb(0, 0, 0);
  border-radius: 0.7em 0.7em 0.7em 0.7em;
  box-shadow: 0 0 0.1em 0.1em rgb(187, 187, 187) inset;
  cursor: pointer;
  display: inline-block;

  height: 1em;
  margin: 0.1em;
  width: 2.3em;
}
.toggle-container input[type="checkbox"]:checked + .toggle-button {
  left: 51%;
  top: 0;
}
.toggle-container input[type="checkbox"]:focus + .toggle-button, .toggle-container:hover .toggle-button {
  border: 1px solid rgba(204, 204, 204, 0.8);
  box-shadow: 0 0 0.2em rgb(102, 102, 102);
}
.toggle-container.vertical input[type="checkbox"]:checked + .toggle-button {
  left: 0;
  top: 51%;
}
.toggle-container.vertical {
  height: 2.3em;
  width: 1em;
}


That's pretty much it. Of course you should probably add labels for the each of the two positions to reduce cognitive overhead - that code is in the basic prototype...and you'll want to add a little JavaScript - at least enough so that someone tapping or clicking on the button can toggle the state - that's as simple as adding an event listener for a click event on the channel (container) element.

Of course you can add a lot of JavaScript to track mouse movement and click events to make the interaction richer more complex, e.g. so that it doesn't change the state when the user releases the mouse button in the same location. If you're interested in the full-on JavaScript solution, that is shown in the complex example that draws on the cjl-slidertoggle.js library (available in the cathmhaol.com collection) - the library also includes functions to include the style rules required by the markup the object generates, which is a little interesting and probably deserves its own blog post - but that's for another day.

One other note - this won't work directly with IE 7 or IE 8 because they don't support the pseudo-class "checked". To get it to work with IE 7 and 8, change the pseudo-class for input[type="checkbox"]:checked to input[type="checkbox"].checked and add JavaScript to change the class when the checkbox is checked.



Demo

This demo uses HTML and CSS only, no JavaScript.


No comments:

Post a Comment