├── README.md ├── docs.css ├── index.html ├── simple-paper-spinner.css └── simple-paper-spinner.gif /README.md: -------------------------------------------------------------------------------- 1 | ![Simple paper-spinner](../gh-pages/simple-paper-spinner.gif) 2 | 3 | # Simple `` 4 | 5 | Intrigued by Keanu Lee's amazing [article](//blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html) describing how he built the official [`` Polymer element](//polymer.github.io/paper-spinner/components/paper-spinner/demo.html) seen in the [Google Material Design spec](//www.google.com/design/spec/components/progress-activity.html), I had to see if it was possible to simplify the spinner further by condensing it down to a single HTML element. My goal was to rebuild the paper-spinner in a way that was: 6 | 7 | * Accessible ✓ 8 | * Cross browser compatible (IE10+) ✓* 9 | * A single HTML element ✓ 10 | * HTML and CSS only ✓ 11 | * Uses relative units for easy scalability ✓ 12 | * Animated with strictly composite properties (`transform` and `opacity`) ✓** 13 | 14 | Well…I got really close. Check it out. (Pull requests are more than welcome.) 15 | 16 | * * * 17 | 18 | *I use `clip-path: circle()`, which is not available in IE or Firefox, as a progressive enhancement only to remove small artifacts left over from the animations. 19 | 20 | **All spinners animate with composite properties except the multicolor spinner. The multicolor spinner unfortunately invokes repaints on color changes. :/ On the plus side, I haven't seen the frame rate dip below 60fps. 21 | 22 | 23 | 24 | ## License 25 | 26 | The MIT License (MIT) 27 | 28 | Copyright (c) 2015-2020 [Chris Nager](https://twitter.com/chrisnager) 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /docs.css: -------------------------------------------------------------------------------- 1 | html { 2 | font: 300 1em/1.4 "Helvetica Neue", sans-serif; 3 | color: #114; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | } 9 | 10 | header, 11 | main, 12 | footer { 13 | padding: 2em; 14 | } 15 | 16 | header, 17 | footer { 18 | background-color: #eee; 19 | } 20 | 21 | h1, 22 | h2 { 23 | font-weight: 500; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | color: #0f9d58; 29 | } 30 | 31 | a:hover, 32 | a:focus { 33 | text-decoration: underline; 34 | } 35 | 36 | @media (min-width: 30em) { 37 | br { 38 | display: none; 39 | } 40 | } 41 | 42 | ul { 43 | padding-left: 30px; 44 | } 45 | 46 | button { 47 | margin: 1em 0; 48 | border: 1px solid; 49 | border-radius: .25em; 50 | padding: 1em; 51 | display: block; 52 | vertical-align: middle; 53 | font-size: .75em; 54 | cursor: pointer; 55 | color: #0f9d58; 56 | background-color: transparent; 57 | -webkit-appearance: none; 58 | } 59 | 60 | button:hover, button:focus { 61 | border-color: #0f9d58; 62 | color: #fff; 63 | background-color: #0f9d58; 64 | } 65 | 66 | pre { 67 | border: 2px solid; 68 | border-radius: .25em; 69 | padding: 1em; 70 | white-space: pre-wrap; 71 | } 72 | 73 | mark { 74 | background-color: rgba(244, 180, 0, .3); 75 | } 76 | 77 | .gh-button { 78 | display: inline-block; 79 | } 80 | 81 | .gh-button { 82 | box-sizing: border-box; 83 | height: 30px; 84 | font-weight: bold; 85 | font-size: 11px; 86 | line-height: 14px; 87 | padding: 2px 5px 2px 4px; 88 | color: #333; 89 | text-decoration: none; 90 | text-shadow: 0 1px 0 #fff; 91 | white-space: nowrap; 92 | cursor: pointer; 93 | overflow: hidden; 94 | border-radius: 3px; 95 | 96 | padding: 3px 10px 3px 8px; 97 | font-size: 16px; 98 | line-height: 22px; 99 | border-radius: 4px; 100 | background-color: #eee; 101 | background-image: linear-gradient(#fcfcfc, #eee); 102 | border: 1px solid #d5d5d5; 103 | } 104 | 105 | .gh-button:hover, 106 | .gh-button:focus { 107 | text-decoration: none; 108 | background-color: #ddd; 109 | background-image: linear-gradient(#eee, #ddd); 110 | border-color: #ccc; 111 | } 112 | 113 | .gh-button:active { 114 | background: #dcdcdc; 115 | border-color: #b5b5b5; 116 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, .15); 117 | } 118 | 119 | .gh-button__icon { 120 | display: inline-block; 121 | vertical-align: top; 122 | width: 20px; 123 | height: 20px; 124 | margin-right: 4px; 125 | background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIxMiAxMiA0MCA0MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAxMiAxMiA0MCA0MCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8cGF0aCBmaWxsPSIjMzMzMzMzIiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjINCgljLTUuMywxLjEtNi40LTIuNi02LjQtMi42QzIwLDQxLjYsMTguOCw0MSwxOC44LDQxYy0xLjctMS4yLDAuMS0xLjEsMC4xLTEuMWMxLjksMC4xLDIuOSwyLDIuOSwyYzEuNywyLjksNC41LDIuMSw1LjUsMS42DQoJYzAuMi0xLjIsMC43LTIuMSwxLjItMi42Yy00LjItMC41LTguNy0yLjEtOC43LTkuNGMwLTIuMSwwLjctMy43LDItNS4xYy0wLjItMC41LTAuOC0yLjQsMC4yLTVjMCwwLDEuNi0wLjUsNS4yLDINCgljMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQNCgljMC43LDAuNiwxLjMsMS43LDEuMywzLjVjMCwyLjYsMCw0LjYsMCw1LjJjMCwwLjUsMC40LDEuMSwxLjMsMC45YzcuNS0yLjYsMTMtOS43LDEzLTE4LjFDNTEsMjEuOSw0Mi41LDEzLjQsMzIsMTMuNHoiLz4NCjwvc3ZnPg0K); 126 | background-size: contain; 127 | } 128 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple paper-spinner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Simple
<paper-spinner>

16 | Star 17 |

Intrigued by Keanu Lee's brilliant article describing how he built the official <paper-spinner> Polymer element seen in the Google Material Design spec, I had to see if it was possible to simplify the spinner further by condensing it down to a single HTML element. My goal was to rebuild the paper-spinner in a way that was:

18 |
    19 |
  • Accessible ✓
  • 20 |
  • Cross browser compatible (IE10+) ✓*
  • 21 |
  • A single HTML element ✓
  • 22 |
  • HTML and CSS only ✓
  • 23 |
  • Uses relative units for easy scalability ✓
  • 24 |
  • Animated with strictly composite properties (transform and opacity) ✓**
  • 25 |
26 |

Well…I got really close. Check it out. (Pull requests are more than welcome.)

27 |
28 | 29 |
30 |

Default

31 |
32 |
<div class="spinner" role="progressbar" aria-valuetext="Loading…"></div>
33 | 34 |

Preset colors

35 |
36 |
37 |
38 |
39 |
<div class="spinner" data-options="blue" role="progressbar" aria-valuetext="Loading…"></div>
40 | 41 |

Preset sizes

42 |
43 |
44 |
45 |
46 |
47 |
<div class="spinner" data-options="large" role="progressbar" aria-valuetext="Loading…"></div>
48 | 49 |

Completely custom

50 |
51 |
52 |
53 |
54 |
55 |
56 |
<div class="spinner" data-options="blue" style="font-size:71px;box-shadow:inset 0 0 0 2px" role="progressbar" aria-valuetext="Loading…"></div>
57 | 58 |

Multicolor

59 | 60 |
61 |
62 |
<div class="spinner" data-options="multicolor" role="progressbar" aria-valuetext="Loading…"></div>
63 | 64 |

How it's done

65 |
66 |
67 |
68 |
69 |
70 | 71 |

Outstanding resources

72 | 80 |
81 |

*I use clip-path: circle(), which is not available in IE or Firefox, as a progressive enhancement only to remove small artifacts left over from the animations.

82 |

**All spinners animate with composite properties except the multicolor spinner. The multicolor spinner unfortunately invokes repaints on color changes. :/ On the plus side, I haven't seen the frame rate dip below 60fps.

83 |
84 | 85 | 88 | 89 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /simple-paper-spinner.css: -------------------------------------------------------------------------------- 1 | /* Spinner rotation */ 2 | @-webkit-keyframes spin { to { -webkit-transform: rotate(1080deg); transform: rotate(1080deg) } } 3 | @keyframes spin { to { -webkit-transform: rotate(1080deg); transform: rotate(1080deg) } } 4 | 5 | .spinner { 6 | width: 1.75em; 7 | height: 1.75em; 8 | border-radius: 50%; 9 | display: inline-block; 10 | position: relative; 11 | overflow: hidden; 12 | box-shadow: inset 0 0 0 .1875em; 13 | will-change: transform; 14 | -webkit-animation: spin 2666ms linear infinite; 15 | animation: spin 2666ms linear infinite; 16 | -webkit-clip-path: circle(.875em at center); 17 | clip-path: circle(.875em at center); 18 | 19 | /* Set background to same color as pseudo elements. Defaults to white */ 20 | background-color: #fff; 21 | } 22 | 23 | /* Preset colors */ 24 | .spinner[data-options*="blue"] { color: #4285f4 } 25 | .spinner[data-options*="red"] { color: #db4437 } 26 | .spinner[data-options*="yellow"] { color: #f4b400 } 27 | .spinner[data-options*="green"] { color: #0f9d58 } 28 | 29 | /* Preset sizes */ 30 | .spinner[data-options*="large"] { font-size: 2em } 31 | .spinner[data-options*="xlarge"] { font-size: 3em } 32 | 33 | /* Multicolor */ 34 | @-webkit-keyframes color-shift { 35 | /* Blue */ 36 | from { box-shadow: inset 0 0 0 .1875em #4285f4 } 37 | 22% { box-shadow: inset 0 0 0 .1875em #4285f4 } 38 | 39 | /* Red */ 40 | 23% { box-shadow: inset 0 0 0 .1875em #db4437 } 41 | 48% { box-shadow: inset 0 0 0 .1875em #db4437 } 42 | 43 | /* Yellow */ 44 | 49% { box-shadow: inset 0 0 0 .1875em #f4b400 } 45 | 73% { box-shadow: inset 0 0 0 .1875em #f4b400 } 46 | 47 | /* Green */ 48 | 74% { box-shadow: inset 0 0 0 .1875em #0f9d58 } 49 | 98% { box-shadow: inset 0 0 0 .1875em #0f9d58 } 50 | 51 | /* Back to blue */ 52 | to { box-shadow: inset 0 0 0 .1875em #4285f4 } 53 | } 54 | @keyframes color-shift { 55 | /* Blue */ 56 | from { box-shadow: inset 0 0 0 .1875em #4285f4 } 57 | 22% { box-shadow: inset 0 0 0 .1875em #4285f4 } 58 | 59 | /* Red */ 60 | 23% { box-shadow: inset 0 0 0 .1875em #db4437 } 61 | 48% { box-shadow: inset 0 0 0 .1875em #db4437 } 62 | 63 | /* Yellow */ 64 | 49% { box-shadow: inset 0 0 0 .1875em #f4b400 } 65 | 73% { box-shadow: inset 0 0 0 .1875em #f4b400 } 66 | 67 | /* Green */ 68 | 74% { box-shadow: inset 0 0 0 .1875em #0f9d58 } 69 | 98% { box-shadow: inset 0 0 0 .1875em #0f9d58 } 70 | 71 | /* Back to blue */ 72 | to { box-shadow: inset 0 0 0 .1875em #4285f4 } 73 | } 74 | 75 | /* Forced to duplicate `spin` keyframes for multicolor spinner. 76 | * Otherwise, spinner continues to rotate without resetting to account for color changes */ 77 | @-webkit-keyframes multicolor-spin { to { -webkit-transform: rotate(1080deg); transform: rotate(1080deg) } } 78 | @keyframes multicolor-spin { to { -webkit-transform: rotate(1080deg); transform: rotate(1080deg) } } 79 | 80 | .spinner[data-options*="multicolor"] { will-change: transform, box-shadow } 81 | .spinner[data-options*="multicolor"].is-active { -webkit-animation: multicolor-spin 2666ms linear infinite, color-shift 8531.2ms infinite linear; animation: multicolor-spin 2666ms linear infinite, color-shift 8531.2ms infinite linear } 82 | 83 | .spinner::before, 84 | .spinner::after { 85 | content: ""; 86 | position: absolute; 87 | width: 7em; 88 | height: 1.75em; 89 | background-color: inherit; 90 | } 91 | 92 | @-webkit-keyframes shape-shift-before { to { -webkit-transform: skewX(-45deg) translate(.875em, -.875em); transform: skewX(-45deg) translate(.875em, -.875em) } } 93 | 94 | @keyframes shape-shift-before { to { -webkit-transform: skewX(-45deg) translate(.875em, -.875em); transform: skewX(-45deg) translate(.875em, -.875em) } } 95 | 96 | .spinner::before { 97 | -webkit-transform: skewX(80deg) translate(.875em, -.875em); 98 | -ms-transform: skewX(80deg) translate(.875em, -.875em); 99 | transform: skewX(80deg) translate(.875em, -.875em); 100 | -webkit-animation: shape-shift-before 1066.4ms cubic-bezier(.4, 0, .2, 1) infinite alternate; 101 | animation: shape-shift-before 1066.4ms cubic-bezier(.4, 0, .2, 1) infinite alternate; 102 | } 103 | 104 | @-webkit-keyframes shape-shift-after { to { -webkit-transform: skewX(45deg) translate(.875em, .875em); transform: skewX(45deg) translate(.875em, .875em) } } 105 | 106 | @keyframes shape-shift-after { to { -webkit-transform: skewX(45deg) translate(.875em, .875em); transform: skewX(45deg) translate(.875em, .875em) } } 107 | 108 | .spinner::after { 109 | -webkit-transform: skewX(-80deg) translate(.875em, .875em); 110 | -ms-transform: skewX(-80deg) translate(.875em, .875em); 111 | transform: skewX(-80deg) translate(.875em, .875em); 112 | -webkit-animation: shape-shift-after 1066.4ms cubic-bezier(.4, 0, .2, 1) infinite alternate; 113 | animation: shape-shift-after 1066.4ms cubic-bezier(.4, 0, .2, 1) infinite alternate; 114 | } 115 | 116 | /* Debug */ 117 | .spinner[data-options*="debug"] { 118 | margin-top: 1em; 119 | margin-bottom: 1em; 120 | } 121 | 122 | .spinner[data-options*="debug"]::before { background-color: rgba(255, 0, 0, .75) } 123 | .spinner[data-options*="debug"]::after { background-color: rgba(0, 0, 255, .75) } 124 | 125 | .spinner[data-options*="debug--1"] { 126 | margin-right: 7em; 127 | overflow: visible; 128 | -webkit-clip-path: none; 129 | clip-path: none; 130 | } 131 | 132 | .spinner[data-options*="debug--1"], 133 | .spinner[data-options*="debug--2"] { -webkit-animation: none; animation: none } 134 | -------------------------------------------------------------------------------- /simple-paper-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisnager/simple-paper-spinner/eca8f45e838072f8b28bfe526986ced5c81b08eb/simple-paper-spinner.gif --------------------------------------------------------------------------------