iOS-style Toggle Switches with CSS

The other day, a coworker ran into a bit of a snag when trying to create form elements (checkboxes) in the style of the toggle switches found in the iOS 5 settings menus. Basically, the hiccups were coming from (what I think is) a bug in webkit browsers that causes children of elements with rounded corners from being fully clipped when any position or transform properties are applied. This doesn't really mater if no background colors or images are applied to the child elements.. if it does, this is what heppens:

rounded corners not clipping content

But you see, if we removing the position property (or transform, or whatever) it looks correct:

rounded corners clipping content

So basically, the original idea of applying the background to the label element (with the text on, off and the handle) and sliding that thing back and forth as a single unit went out the window, because the requires using a transform (or positioning, at the very least.)

Then we came up with this:

  • Some Setting


The trick here was not to apply the background to the <label> element, but rather one of its siblings. And instead of animating the element, we animated the background position. Then, removing the background on the <label> allowed us to apply a transformation to that element without running into the problem described above.

I went back and forth on the html structure for a little while, and finally settled on this:

<div class="toggle-switch">
	<input type="checkbox" id="toggle-1" checked="checked" />
	<div class="bg"></div>
	<label for="toggle-1"><span>ON</span><span>OFF</span></label>
</div>

Making the <input element a sibling to the background element and label comes with the added benefit of writing css rules on the :checked attribute of the input element. Such as this:

.toggle-switch input:checked ~ .bg { background-position-x:20%; background-position:20% 50%; }
.toggle-switch input:checked ~ label { -webkit-transform:translateX(0); -moz-transform:translateX(0); transform:translateX(0);}
.toggle-switch input:checked ~ label span:last-child { -webkit-opacity:0; opacity:0; -webkit-transition-delay:0s; -moz-transition-delay:0s; transition-delay:0s; }

In fact, you almost don't need any javascript for this to work because of the native behavior of a label element associated with a form element. However, I ran into a problem in mobile Safari where labels wouldn't control a form element that was out of view.

Here's the full css:

.toggle-switch {
    width:73px;
    height:28px;
    margin:20px;
    padding:3px;
    overflow:hidden;
    -webkit-transform:translateZ(0); /* eliminates flickering */
}

.toggle-switch .bg {
    width:73px; 
    height:28px;
    margin-bottom:-27px;
    -webkit-border-radius:14px; -moz-border-radius:14px; border-radius:14px;
    background-image:-webkit-gradient(linear,0 0, 100% 0, from(#08c), color-stop(50%, #08c), color-stop(51%, #fefefe), to(#eee));
    background-image:-moz-linear-gradient(0deg, #08c 0, #08c 50%, #fefefe 51%, #eee 100%);
    background-image:-linear-gradient(#08c, #08c 50%, #fefefe 51%, #eee);
    -webkit-background-size:200%; -moz-background-size:200%; background-size:200%;
    -webkit-transition:background-position 0.16s linear;
       -moz-transition:background-position 0.2s linear;
            transition:background-position 0.2s linear;
    background-position:85%;
    -webkit-box-shadow:inset 0 0 4px rgba(0,0,0,0.7), inset 0 14px 0 rgba(0,0,0,0.1);
       -moz-box-shadow:inset 0 0 4px rgba(0,0,0,0.7), inset 0 14px 0 rgba(0,0,0,0.1);
            box-shadow:inset 0 0 4px rgba(0,0,0,0.7), inset 0 14px 0 rgba(0,0,0,0.1);
}

.toggle-switch label { display:block; display:-webkit-box; display:-moz-box; display:box; width:78px; height:28px; position:relative; z-index:1;
    -webkit-box-align:center; -moz-box-align:center; box-align:center;
    -webkit-box-orient:horizontal; -moz-box-orient:horizontal; box-orient:horizontal;
    -webkit-transform:translateX(-47px); -moz-transform:translateX(-47px); transform:translateX(-50%);
    -webkit-transition:-webkit-transform 0.19s linear; -moz-transition:-moz-transform 0.2s linear; transition:transform 0.2s linear;
    -webkit-tap-highlight-color:transparent;
}

.toggle-switch input:checked ~ .bg { background-position-x:20%; background-position:20% 50%; }
.toggle-switch input:checked ~ label { -webkit-transform:translateX(0); -moz-transform:translateX(0); transform:translateX(0);}
.toggle-switch input:checked ~ label span:last-child { -webkit-opacity:0; opacity:0; -webkit-transition-delay:0s; -moz-transition-delay:0s; transition-delay:0s; }

.toggle-switch input { display:none; }

.toggle-switch label span {
    display:block; width:50%; padding:0 15px; font-size:14px; font-weight:bold; line-height:26px;
    -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box;
}

.toggle-switch label span:first-child { position:relative; color:#fff; padding-right:0; width:46px; }

.toggle-switch label span:last-child {
    padding-left:33px;
    padding-right:0;
    color:#666;
    text-shadow:1px 1px 0 #fff;
    -webkit-opacity:0.7; -moz-opacity:0.7; opacity:0.7;
    -webkit-transition:-webkit-opacity 0.2s 0.1s linear; -moz-transition:-moz-opacity 0.2s linear; transition:opacity 0.2s 0.1s linear;
}

.toggle-switch label span:first-child::after {
    position:absolute;
    right:-28px;
    top:-2px;
    display:inline-block;
    width:28px;
    height:28px;
    border:1px solid #666;
    -webkit-border-radius:14px; -moz-border-radius:14px; border-radius:14px;
    background-color:#ccc;
    -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box;
    content:"";
    -webkit-box-shadow:0 0 2px rgba(0,0,0,0.7), inset 0 0 3px 1px rgba(255,255,255,1);
       -moz-box-shadow:0 0 2px rgba(0,0,0,0.7), inset 0 0 3px 1px rgba(255,255,255,1);
            box-shadow:0 0 2px rgba(0,0,0,0.7), inset 0 0 3px 1px rgba(255,255,255,1);
    background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#bbb), to(#fefefe));
    background-image:-moz-linear-gradient(#bbb, #fefefe);
    background-image:linear-gradient(#bbb, #fefefe);
}

And just a little javascript to make sure that the checked attribute is set on the checkbox.

;(function($, window, undefined) {
	$(document).ready(function() {
		$('.toggle-switch label').bind('click', function(e) {
			var $checkbox = $(this).siblings('input');
			e.preventDefault();
			$checkbox.attr('checked', !$checkbox.attr('checked'));
		});
	});
})(jQuery, this);

And that's pretty much it. View full-page demo

Comments

J B
Pretty cool, but doesn't slide all the way over in FF (I'm using FF 17.0.1 on a Mac).
Darius
Great! :D ran into the same bug with a similar code. @J B - I know its not the best way to fix it but adding this mozilla specific css might do the trick. @-moz-document url-prefix() { .toggle-switch .bg {margin-left: 7px; width: 67px;} }
Martin stehouwer

Add new comment