diff --git a/app/styleguide.html b/app/styleguide.html
index 2ea12594..fecc924b 100644
--- a/app/styleguide.html
+++ b/app/styleguide.html
@@ -101,6 +101,11 @@
+
Slider
diff --git a/app/styleguide/_colors.scss b/app/styleguide/_colors.scss
index 2a45a496..e9dc4be9 100644
--- a/app/styleguide/_colors.scss
+++ b/app/styleguide/_colors.scss
@@ -83,6 +83,17 @@ $checkbox-color: nth($palette-primary, 6);
$checkbox-off-color: rgba(0, 0, 0, 0.54);
$checkbox-disabled-color: rgba(0, 0, 0, 0.26);
+/* ========== Switches ========== */
+
+$switch-color: nth($palette-primary, 6);
+$switch-thumb-color: $switch-color;
+$switch-track-color: rgba($switch-color, 0.5);
+
+$switch-off-thumb-color: nth($palette-grey, 1);
+$switch-off-track-color: rgba(0, 0, 0, 0.26);
+$switch-disabled-thumb-color: nth($palette-grey, 5);
+$switch-disabled-track-color: rgba(0, 0, 0, 0.12);
+
/* ========== Text fields ========== */
$input-text-background-color: transparent;
diff --git a/app/styleguide/switch/_switch.scss b/app/styleguide/switch/_switch.scss
new file mode 100644
index 00000000..2957bb91
--- /dev/null
+++ b/app/styleguide/switch/_switch.scss
@@ -0,0 +1,181 @@
+@import "../colors";
+@import "../animation/animation";
+@import "../shadow/shadow";
+@import "../ripple/ripple";
+
+$switch-label-height: 24px;
+$switch-track-height: 14px;
+$switch-track-length: 36px;
+$switch-thumb-size: 20px;
+$switch-track-top: ($switch-label-height - $switch-track-height) / 2;
+$switch-thumb-top: ($switch-label-height - $switch-thumb-size) / 2;
+$switch-ripple-size: $switch-label-height * 2;
+
+.wsk-switch {
+ position: relative;
+
+ z-index: 1;
+
+ vertical-align: middle;
+
+ display: inline-block;
+
+ box-sizing: border-box;
+ width: 100%;
+ height: $switch-label-height;
+ margin: 12px 0;
+ padding: 0;
+
+ overflow: visible;
+}
+
+.wsk-switch__input {
+ line-height: $switch-label-height;
+
+ .wsk-switch.is-upgraded & {
+ // Hide input element, while still making it respond to focus.
+ position: absolute;
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+ opacity: 0;
+ -ms-appearance: none;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ border: none;
+ }
+}
+
+.wsk-switch__track {
+ background: $switch-off-track-color;
+ position: absolute;
+ left: 0;
+ top: $switch-track-top;
+ height: $switch-track-height;
+ width: $switch-track-length;
+ border-radius: $switch-track-height;
+
+ cursor: pointer;
+
+ .wsk-switch.is-checked & {
+ background: $switch-track-color;
+ }
+
+ .wsk-switch.is-disabled & {
+ background: $switch-disabled-track-color;
+ cursor: auto;
+ }
+}
+
+.wsk-switch__thumb {
+ background: $switch-off-thumb-color;
+ position: absolute;
+ left: 0;
+ top: $switch-thumb-top;
+ height: $switch-thumb-size;
+ width: $switch-thumb-size;
+ border-radius: 50%;
+
+ cursor: pointer;
+
+ @include shadow-z1();
+
+ @include material-animation-default(0.28s);
+ transition-property: left;
+
+ .wsk-switch.is-checked & {
+ background: $switch-thumb-color;
+ left: $switch-track-length - $switch-thumb-size;
+
+ @include shadow-z2();
+ }
+
+ .wsk-switch.is-disabled & {
+ background: $switch-disabled-thumb-color;
+ cursor: auto;
+ }
+}
+
+.wsk-switch__focus-helper {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+
+ transform: translate(-$switch-button-size / 2, -$switch-button-size / 2);
+
+ display: inline-block;
+
+ box-sizing: border-box;
+ width: $switch-button-size;
+ height: $switch-button-size;
+ border-radius: 50%;
+
+ background-color: transparent;
+
+ .wsk-switch.is-focused & {
+ box-shadow: 0 0 0px (($switch-ripple-size - $switch-button-size) / 2)
+ rgba(0, 0, 0, 0.1);
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ .wsk-switch.is-focused.is-checked & {
+ box-shadow: 0 0 0px (($switch-ripple-size - $switch-button-size) / 2)
+ rgba($switch-color, 0.26);
+ background-color: rgba($switch-color, 0.26);
+ }
+}
+
+.wsk-switch__label {
+ position: relative;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: $switch-label-height;
+ margin: 0;
+ left: 24px;
+
+ .wsk-switch.is-disabled & {
+ color: $switch-disabled-thumb-color;
+ cursor: auto;
+ }
+}
+
+.wsk-switch__ripple-container {
+ position: absolute;
+ z-index: 2;
+ top: -($switch-ripple-size - $switch-label-height) / 2;
+ left: $switch-thumb-size / 2 - $switch-ripple-size / 2;
+
+ box-sizing: border-box;
+ width: $switch-ripple-size;
+ height: $switch-ripple-size;
+ border-radius: 50%;
+
+ cursor: pointer;
+
+ overflow: hidden;
+ -webkit-mask-image: -webkit-radial-gradient(circle, white, black);
+
+ transition-duration: 0.40s;
+ transition-timing-function: step-end;
+ transition-property: left;
+
+ & .wsk-ripple {
+ background: $switch-color;
+ }
+
+ .wsk-switch.is-disabled & {
+ cursor: auto;
+ }
+
+ .wsk-switch.is-disabled & .wsk-ripple {
+ background: transparent;
+ }
+
+ .wsk-switch.is-checked & {
+ cursor: auto;
+ left: $switch-track-length - $switch-ripple-size / 2 -
+ $switch-thumb-size / 2;
+ }
+}
diff --git a/app/styleguide/switch/demo.html b/app/styleguide/switch/demo.html
new file mode 100644
index 00000000..fe535c2d
--- /dev/null
+++ b/app/styleguide/switch/demo.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
Switch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/styleguide/switch/demo.scss b/app/styleguide/switch/demo.scss
new file mode 100644
index 00000000..5073f66a
--- /dev/null
+++ b/app/styleguide/switch/demo.scss
@@ -0,0 +1,2 @@
+@import "../styleguide_demo_bp";
+@import "_switch";
diff --git a/app/styleguide/switch/switch.js b/app/styleguide/switch/switch.js
new file mode 100644
index 00000000..fb49caf0
--- /dev/null
+++ b/app/styleguide/switch/switch.js
@@ -0,0 +1,220 @@
+/**
+ * Class constructor for Checkbox WSK component.
+ * Implements WSK component design pattern defined at:
+ * https://github.com/jasonmayes/wsk-component-design-pattern
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+function MaterialSwitch(element) {
+ 'use strict';
+
+ this.element_ = element;
+
+ // Initialize instance.
+ this.init();
+}
+
+/**
+ * Store constants in one place so they can be updated easily.
+ * @enum {string | number}
+ * @private
+ */
+MaterialSwitch.prototype.Constant_ = {
+ TINY_TIMEOUT: 0.001
+};
+
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ * @enum {string}
+ * @private
+ */
+MaterialSwitch.prototype.CssClasses_ = {
+ /**
+ * Class names should use camelCase and be prefixed with the word "material"
+ * to minimize conflict with 3rd party systems.
+ */
+
+ // TODO: Upgrade classnames in HTML / CSS / JS to use material prefix to
+ // reduce conflict and convert to camelCase for consistency.
+ WSK_SWITCH_INPUT: 'wsk-switch__input',
+
+ WSK_SWITCH_TRACK: 'wsk-switch__track',
+
+ WSK_SWITCH_THUMB: 'wsk-switch__thumb',
+
+ WSK_SWITCH_FOCUS_HELPER: 'wsk-switch__focus-helper',
+
+ WSK_JS_RIPPLE_EFFECT: 'wsk-js-ripple-effect',
+
+ WSK_JS_RIPPLE_EFFECT_IGNORE_EVENTS: 'wsk-js-ripple-effect--ignore-events',
+
+ WSK_SWITCH_RIPPLE_CONTAINER: 'wsk-switch__ripple-container',
+
+ WSK_RIPPLE_CENTER: 'wsk-ripple--center',
+
+ WSK_RIPPLE: 'wsk-ripple',
+
+ IS_FOCUSED: 'is-focused',
+
+ IS_DISABLED: 'is-disabled',
+
+ IS_CHECKED: 'is-checked'
+};
+
+
+/**
+ * Handle change of state.
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onChange_ = function(event) {
+ 'use strict';
+
+ this.updateClasses_(this.btnElement_, this.element_);
+};
+
+
+/**
+ * Handle focus of element.
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onFocus_ = function(event) {
+ 'use strict';
+
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+
+
+/**
+ * Handle lost focus of element.
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onBlur_ = function(event) {
+ 'use strict';
+
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+
+
+/**
+ * Handle mouseup.
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onMouseUp_ = function(event) {
+ 'use strict';
+
+ this.blur_();
+};
+
+
+/**
+ * Handle class updates.
+ * @param {HTMLElement} button The button whose classes we should update.
+ * @param {HTMLElement} label The label whose classes we should update.
+ * @private
+ */
+MaterialSwitch.prototype.updateClasses_ = function(button, label) {
+ 'use strict';
+
+ if (button.disabled) {
+ label.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ label.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+
+ if (button.checked) {
+ label.classList.add(this.CssClasses_.IS_CHECKED);
+ } else {
+ label.classList.remove(this.CssClasses_.IS_CHECKED);
+ }
+};
+
+
+/**
+ * Add blur.
+ * @private
+ */
+MaterialSwitch.prototype.blur_ = function(event) {
+ 'use strict';
+
+ // TODO: figure out why there's a focus event being fired after our blur,
+ // so that we can avoid this hack.
+ window.setTimeout(function() {
+ this.btnElement_.blur();
+ }.bind(this), this.Constant_.TINY_TIMEOUT);
+};
+
+
+/**
+ * Initialize element.
+ */
+MaterialSwitch.prototype.init = function() {
+ 'use strict';
+
+ if (this.element_) {
+ this.btnElement_ = this.element_.querySelector('.' +
+ this.CssClasses_.WSK_SWITCH_INPUT);
+
+ var track = document.createElement('div');
+ track.classList.add(this.CssClasses_.WSK_SWITCH_TRACK);
+
+ var thumb = document.createElement('div');
+ thumb.classList.add(this.CssClasses_.WSK_SWITCH_THUMB);
+
+ var focusHelper = document.createElement('span');
+ focusHelper.classList.add(this.CssClasses_.WSK_SWITCH_FOCUS_HELPER);
+
+ thumb.appendChild(focusHelper);
+
+ this.element_.appendChild(track);
+ this.element_.appendChild(thumb);
+
+ var rippleContainer;
+ if (this.element_.classList.contains(
+ this.CssClasses_.WSK_JS_RIPPLE_EFFECT)) {
+ this.element_.classList.add(
+ this.CssClasses_.WSK_JS_RIPPLE_EFFECT_IGNORE_EVENTS);
+ rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(
+ this.CssClasses_.WSK_SWITCH_RIPPLE_CONTAINER);
+ rippleContainer.classList.add(this.CssClasses_.WSK_JS_RIPPLE_EFFECT);
+ rippleContainer.classList.add(this.CssClasses_.WSK_RIPPLE_CENTER);
+
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.WSK_RIPPLE);
+
+ rippleContainer.appendChild(ripple);
+ this.element_.appendChild(rippleContainer);
+ }
+
+ this.btnElement_.addEventListener('change', this.onChange_.bind(this));
+
+ this.btnElement_.addEventListener('focus', this.onFocus_.bind(this));
+
+ this.btnElement_.addEventListener('blur', this.onBlur_.bind(this));
+
+ this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this));
+
+ rippleContainer.addEventListener('mouseup', this.onMouseUp_.bind(this));
+
+ this.updateClasses_(this.btnElement_, this.element_);
+ this.element_.classList.add('is-upgraded');
+ }
+};
+
+
+window.addEventListener('load', function() {
+ 'use strict';
+
+ // On document ready, the component registers itself. It can assume
+ // componentHandler is available in the global scope.
+ componentHandler.register({
+ constructor: MaterialSwitch,
+ classAsString: 'MaterialSwitch',
+ cssClass: 'wsk-js-switch'
+ });
+});