diff --git a/ionic/components.core.scss b/ionic/components.core.scss
index b34aa092cd..44814293c9 100644
--- a/ionic/components.core.scss
+++ b/ionic/components.core.scss
@@ -20,7 +20,8 @@
"components/modal/modal",
"components/scroll/scroll",
"components/scroll/pull-to-refresh",
- "components/slides/slides";
+ "components/slides/slides",
+ "components/spinner/spinner";
// Ionicons (to be replaced with SVGs)
diff --git a/ionic/components/spinner/spinner.scss b/ionic/components/spinner/spinner.scss
new file mode 100644
index 0000000000..cce517e8b4
--- /dev/null
+++ b/ionic/components/spinner/spinner.scss
@@ -0,0 +1,119 @@
+
+
+// Spinners
+// --------------------------------------------------
+
+ion-spinner {
+ display: inline-block;
+ position: relative;
+ width: 28px;
+ height: 28px;
+}
+
+ion-spinner svg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform: translateZ(0);
+}
+
+ion-spinner.spinner-paused svg {
+ animation-play-state: paused;
+}
+
+
+// Spinner: ios / ios-small
+// --------------------------------------------------
+
+.spinner-ios line,
+.spinner-ios-small line {
+ stroke: #69717d;;
+ stroke-width: 4px;
+ stroke-linecap: round;
+}
+
+.spinner-ios svg,
+.spinner-ios-small svg {
+ animation: spinner-fade-out 1s linear infinite;
+}
+
+
+// Spinner: bubbles
+// --------------------------------------------------
+
+.spinner-bubbles circle {
+ fill: black;
+}
+
+.spinner-bubbles svg {
+ animation: spinner-scale-out 1s linear infinite;
+}
+
+
+// Spinner: circles
+// --------------------------------------------------
+
+.spinner-circles circle {
+ fill: #69717d;
+}
+
+.spinner-circles svg {
+ animation: spinner-fade-out 1s linear infinite;
+}
+
+
+// Spinner: crescent
+// --------------------------------------------------
+
+.spinner-crescent circle {
+ fill: transparent;
+ stroke: black;
+ stroke-width: 4px;
+ stroke-dasharray: 128px;
+ stroke-dashoffset: 82px;
+}
+
+.spinner-crescent svg {
+ animation: spinner-rotate 1s linear infinite;
+}
+
+
+// Spinner: dots
+// --------------------------------------------------
+
+.spinner-dots circle {
+ fill: #444;
+ stroke-width: 0;
+}
+
+.spinner-dots svg {
+ animation: spinner-dots 1s linear infinite;
+ transform-origin: center;
+}
+
+
+// Animation Keyframes
+// --------------------------------------------------
+
+@keyframes spinner-fade-out {
+ 0% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes spinner-scale-out {
+ 0% { transform: scale(1, 1); }
+ 100% { transform: scale(0, 0); }
+}
+
+@keyframes spinner-rotate {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes spinner-dots {
+ 0% { opacity: 0.9; transform: scale(1, 1); }
+ 50% { opacity: 0.3; transform: scale(0.4, 0.4); }
+ 100% { opacity: 0.9; transform: scale(1, 1); }
+}
diff --git a/ionic/components/spinner/spinner.ts b/ionic/components/spinner/spinner.ts
new file mode 100644
index 0000000000..1a300d7402
--- /dev/null
+++ b/ionic/components/spinner/spinner.ts
@@ -0,0 +1,283 @@
+import {Component, Input} from 'angular2/core';
+import {NgStyle} from 'angular2/common';
+
+import {Config} from '../../config/config';
+
+
+/**
+ * @name Spinner
+ * @description
+ * The `ion-spinner` component provides a variety of animated SVG spinners.
+ * Spinners enables you to give users feedback that the app is actively
+ * processing/thinking/waiting/chillin’ out, or whatever you’d like it to indicate.
+ * By default, the `ion-refresher` feature uses this spinner component while it's
+ * the refresher is in the `refreshing` state.
+ *
+ * Ionic offers a handful of spinners out of the box, and by default, it will use
+ * the appropriate spinner for the platform on which it’s running.
+ *
+ *
+ *
+ *
+ * ios
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * ios-small
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * bubbles
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * circles
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * crescent
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * dots
+ * |
+ *
+ *
+ * |
+ *
+ *
+ *
+ * @usage
+ * The following code would use the default spinner for the platform it's
+ * running from. If it's neither iOS or Android, it'll default to use `ios`.
+ *
+ * ```html
+ *
+ * ```
+ *
+ * By setting the `name` property, you can specify which predefined spinner to
+ * use, no matter what the platform is.
+ *
+ * ```html
+ *
+ * ```
+ *
+ * ## Styling SVG with CSS
+ * One cool thing about SVG is its ability to be styled with CSS! One thing to note
+ * is that some of the CSS properties on an SVG element have different names. For
+ * example, SVG uses the term `stroke` instead of `border`, and `fill` instead
+ * of `background-color`.
+ *
+ * ```css
+ * ion-spinner svg {
+ * width: 28px;
+ * height: 28px;
+ * stroke: #444;
+ * fill: #222;
+ * }
+ * ```
+ */
+@Component({
+ selector: 'ion-spinner',
+ template:
+ '' +
+ '',
+ directives: [NgStyle],
+ host: {
+ '[class]': '_applied',
+ '[class.spinner-paused]': 'paused'
+ }
+})
+export class Spinner {
+ private _c: any[];
+ private _l: any[];
+ private _name: string;
+ private _dur: number = null;
+ private _init: boolean;
+ private _applied: string;
+
+ /**
+ * @input {string} SVG spinner name.
+ */
+ @Input()
+ get name(): string {
+ return this._name;
+ }
+
+ set name(val: string) {
+ this._name = val;
+ this.load();
+ }
+
+ /**
+ * @input {string} How long it takes it to do one loop.
+ */
+ @Input()
+ get duration(): number {
+ return this._dur;
+ }
+
+ set duration(val: number) {
+ this._dur = val;
+ this.load();
+ }
+
+ /**
+ * @input {string} If the animation is paused or not. Defaults to `false`.
+ */
+ @Input() paused: boolean = false;
+
+ constructor(private _config: Config) {}
+
+ ngOnInit() {
+ this._init = true;
+ this.load();
+ }
+
+ load() {
+ if (this._init) {
+ this._l = [];
+ this._c = [];
+
+ var name = this._name || this._config.get('spinner', 'ios');
+
+ const spinner = SPINNERS[name];
+ if (spinner) {
+ this._applied = 'spinner-' + name;
+
+ if (spinner.lines) {
+ for (var i = 0, l = spinner.lines; i < l; i++) {
+ this._l.push( this._loadEle(spinner, i, l) );
+ }
+
+ } else if (spinner.circles) {
+ for (var i = 0, l = spinner.circles; i < l; i++) {
+ this._c.push( this._loadEle(spinner, i, l) );
+ }
+ }
+
+ }
+ }
+ }
+
+ _loadEle(spinner: any, index: number, total: number) {
+ let duration = this._dur || spinner.dur
+ let data = spinner.fn(duration, index, total);
+ data.style.animationDuration = duration + 'ms';
+ return data;
+ }
+
+}
+
+const SPINNERS = {
+
+ ios: {
+ dur: 1000,
+ lines: 12,
+ fn: function(dur, index, total) {
+ return {
+ y1: 17,
+ y2: 29,
+ style: {
+ transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)',
+ animationDelay: -(dur - ((dur / total) * index)) + 'ms'
+ }
+ }
+ }
+ },
+
+ 'ios-small': {
+ dur: 1000,
+ lines: 12,
+ fn: function(dur, index, total) {
+ return {
+ y1: 12,
+ y2: 20,
+ style: {
+ transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)',
+ animationDelay: -(dur - ((dur / total) * index)) + 'ms'
+ }
+ }
+ }
+ },
+
+ bubbles: {
+ dur: 1000,
+ circles: 9,
+ fn: function(dur, index, total) {
+ return {
+ r: 5,
+ style: {
+ top: 9 * Math.sin(2 * Math.PI * index / total),
+ left: 9 * Math.cos(2 * Math.PI * index / total),
+ animationDelay: -(dur - ((dur / total) * index)) + 'ms'
+ }
+ }
+ }
+ },
+
+ circles: {
+ dur: 1000,
+ circles: 8,
+ fn: function(dur, index, total) {
+ return {
+ r: 5,
+ style: {
+ top: 9 * Math.sin(2 * Math.PI * index / total),
+ left: 9 * Math.cos(2 * Math.PI * index / total),
+ animationDelay: -(dur - ((dur / total) * index)) + 'ms'
+ }
+ }
+ }
+ },
+
+ crescent: {
+ dur: 750,
+ circles: 1,
+ fn: function(dur) {
+ return {
+ r: 26,
+ style: {}
+ }
+ }
+ },
+
+ dots: {
+ dur: 750,
+ circles: 3,
+ fn: function(dur, index, total) {
+ return {
+ r: 6,
+ style: {
+ left: (9 - (9 * index)),
+ animationDelay: -(110 * index) + 'ms'
+ }
+ }
+ }
+ }
+
+};
diff --git a/ionic/components/spinner/test/basic/index.ts b/ionic/components/spinner/test/basic/index.ts
new file mode 100644
index 0000000000..e578203808
--- /dev/null
+++ b/ionic/components/spinner/test/basic/index.ts
@@ -0,0 +1,13 @@
+import {App} from 'ionic-angular';
+
+
+@App({
+ templateUrl: 'main.html'
+})
+class E2EApp {
+ paused: boolean = false;
+
+ toggleState() {
+ this.paused = !this.paused;
+ }
+}
diff --git a/ionic/components/spinner/test/basic/main.html b/ionic/components/spinner/test/basic/main.html
new file mode 100644
index 0000000000..af93f162fe
--- /dev/null
+++ b/ionic/components/spinner/test/basic/main.html
@@ -0,0 +1,55 @@
+
+
+ Spinners
+
+
+
+
+
+
+ Platform Default |
+
+
+ |
+
+
+ ios |
+
+
+ |
+
+
+ ios-small |
+
+
+ |
+
+
+ bubbles |
+
+
+ |
+
+
+ circles |
+
+
+ |
+
+
+ crescent |
+
+
+ |
+
+
+ dots |
+
+
+ |
+
+
+
+
+
+
diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts
index 9fc4c72274..dc8da6d53a 100644
--- a/ionic/config/directives.ts
+++ b/ionic/config/directives.ts
@@ -19,6 +19,7 @@ import {Item} from '../components/item/item';
import {ItemSliding} from '../components/item/item-sliding';
import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar';
import {Icon} from '../components/icon/icon';
+import {Spinner} from '../components/spinner/spinner';
import {Checkbox} from '../components/checkbox/checkbox';
import {Select} from '../components/select/select';
import {Option} from '../components/option/option';
@@ -80,6 +81,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when';
*
* **Media**
* - Icon
+ * - Spinner
*
* **Forms**
* - Searchbar
@@ -146,6 +148,7 @@ export const IONIC_DIRECTIVES = [
// Media
Icon,
+ Spinner,
// Forms
Searchbar,
diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts
index 8cba1a0cca..d2dd2355ee 100644
--- a/ionic/config/modes.ts
+++ b/ionic/config/modes.ts
@@ -25,6 +25,8 @@ Config.setModeConfig('ios', {
pageTransition: 'ios-transition',
pageTransitionDelay: 16,
+ spinner: 'ios',
+
tabbarPlacement: 'bottom',
});
@@ -52,6 +54,8 @@ Config.setModeConfig('md', {
pageTransition: 'md-transition',
pageTransitionDelay: 96,
+ spinner: 'crescent',
+
tabbarHighlight: true,
tabbarPlacement: 'top',