Spun-out with Spinner Images?

Try the HTML Spinner Element

Here's the markup for one of the above. Can you see which one? <x-spinner rotor="1111" color="red" wt="3" dir="ccw" rstyle="dotted"></x-spinner>

Quick Start:

  1. Get the script spinnerComponent.js (see Sources, below)
  2. Place it where your scripts are, e.g., in a ./scripts directory
  3. In your HTML, load the script: <script src="./scripts/spinnerComponent.js"></script>
  4. Place a spinner where you want it
  5. Default: <x-spinner></x-spinner>
  6. Fast Dotted Green:
    <x-spinner rstyle="dotted" sp=".5" color="green"></x-spinner>

See Rotor Styles, below, for an organized view of variations.
See the README file in the Sources download for extensive instructions and documentation.


I'm fed up with the limitations of using animated images for "wait spinners". Now I've made a customizable spinner, the SpinnerElement, that works without images in any HTML layout.

Good web interface design shows what's happening. A long background process calls for some kind of indicator, especially if no other action is possible on the page until that process completes (or bails out). The most common indicators are progress bars and wait spinners. Here I'm focused on a better option for wait spinners.

I need ways let my clients know their data-oriented sites are busy, not dead. For convenience, I've settled for a few generic animated GIFs that I bet you've seen. The alternative is designing a new one, or searching through millions of spinner images online. But that's not the whole task...

What if the client has a color scheme the spinner should fit into? Or a typeface whose light weight makes the generic spinner look too bulky? Sizing someone's nice downloaded spinner image to fit its spot in the layout also takes time and attention, especially if it's in a line of text.

Introducing the HTML SpinnerElement
The SpinnerElement is constructed automatically within your HTML when your page loads the Javascript spinnerComponent.js.

Yes, Javascript. Why is loading a Javascript-based spinner better than including an image file?

Spinners do not rely on Javascript to run. When spinnerComponent.js loads, it executes once to construct the SpinnerElement class and attach it to the HTML document. When a spinner is rendered in markup, its internal css provides its format and rotation. spinnerComponent.js also provides an optional programming interface whose methods momentarily execute only when adding or modifying spinners.

Spinners follow ARIA accessibility guidelines. The rotors themselves are invisible to screen readers, because they have no content to read. Any included prefixes and suffixes will be seen and read. The spinner may also be marked as a "live" area by assigning it the attribute aria-wrap="true"; it will then assume the role of communicating changes in status, such as when a long-running process completes.

The SpinnerElement is self-contained. Spinners have no side-effects on the rest of the layout, and multiple spinners in the same layout have no effect on each other. Being encapsulated also keeps the SpinnerElement compatible with other web app resources, scripts, modules, and frameworks. Creation of the SpinnerElement utilizes HTML, css, and Javascript capabilities that are standardized and broadly adopted by web browsers.

Loading cost and time for spinnerComponent.js is minimal, with a file size less than 20k.

Some Examples

<p style="background-color:yellow;padding:2em;margin-left:0;font-size:2em;"> <x-spinner prefix="We'll be right back ... " rotor="1" trace-color="transparent" back-color="white" color="red"></x-spinner> </p>

<p style="color:blue; font-size:2em;"> <x-spinner prefix="Is it moving?" kerning="1ch" rotor="101" wt=".195" bgclr="gold" tclr="red" bkclr="rgba(0,55,255,.25)"></x-spinner> </p>

<p style="font-size:2em;"> <x-spinner onclick="this.stopGo()" rotor="101" wt=".495" sp="2" tclr="rgba(255,255,0,1)" kern="1ch" suffix="Restricted"></x-spinner> </p>

<p style="font-size:2em;"> <x-spinner onclick="this.stopGo()" rotor="101" rstyle="dotted" wt="3" sp="1" tclr="rgba(255,51,0,1)" kern="1ch" suffix="Click to Start/Stop"></x-spinner> </p>

Recording in Progress

<p style="font-size:2.4em;color:#ff0000;"> <x-spinner color="#ff0000" rotor="101" wt="8" speed="1" direction="cw" back-color=transparent ></x-spinner> Recording in Progress <x-spinner color="#ff0000" rotor="0101" wt="8" speed="1" direction="cw" back-color=transparent "></x-spinner> </p>
<div style="color:rgba(92, 51, 23, 1); font-size:2em;"> <x-spinner prefix="Research " suffix=" takes time." kern="0ch" wt=".195" speed=".75" rotor="1110" direction="cw" back-color="rgba(92, 51, 23, 0.2)" trace-color="transparent"></x-spinner> </div>

<button type="button" onclick=" const spin6 = document.getElementById('sp6'); if (this.innerHTML === 'Chase') { // set attributes individually with JS built-in .setAttribute spin6.setAttribute('rotor-color', '#00dd00'); spin6.setAttribute('direction', 'cw'); spin6.setAttribute('sp', '.4'); spin6.setAttribute('wt', '3'); spin6.setAttribute('rstyle', 'dotted'); spin6.setAttribute('suf', ' Chasing!'); spin6.style.fontStyle = 'italic'; spin6.style.color = '#00dd00'; this.innerHTML = 'Scan'; } else { // set attributes in one op with spinner's .setAttributes method spin6.setAttributes({ color: '#ddd', direction: 'ccw', sp: '1.5', wt: '8', suf: ' Scanning...', rstyle: 'solid', rstatus: 'running', rotor: '1' }); spin6.style.fontStyle = 'normal'; spin6.style.color = '#ccc'; this.innerHTML = 'Chase'; } // Show the rendered spinner in the console: console.log( spin6.toString() ); ">Scan</button> <p style="font-size:4em;margin-top:0;margin-bottom:0;"> <!-- These attributes define the spinner when first loaded --> <x-spinner id="sp6" rstatus="paused" rotor="1" color="#ddd" sp="1.5" tclr="#ccf" wt="8" dir="ccw" kern="0em" role="status" aria-wrap='true'></x-spinner><br> </p>


<button type="button" onclick=" // Yes, start a spinner element from raw text // Note need to use '&lt;' instead of '<' if (this.innerHTML == 'Search') { let spn = `&lt;x-spinner id='spinMe'>&lt;/x-spinner>`; document.getElementById('searching').innerHTML = spn; const spinM = document.getElementById('spinMe'); spinM.setAttribute('sp', '.5'); spinM.setAttribute('rotor-style', 'double'); spinM.setAttribute('pre', 'Searching ... '); spinM.setAttribute('direction', 'cw'); spinM.setAttribute('trace-color', 'transparent' ); console.log( spinM.toString() ); this.innerHTML = 'Cancel'; } else { document.getElementById('searching').innerHTML = '   '; this.innerHTML = 'Search'; } ">Search</button> <h2 id="searching" style="color:red;">   </h2>


<p style="font-size:3em;margin-top:0;margin-bottom:0;"> <button type="button" style="width:6em;" onclick=" const pausebtn = document.getElementById('pauseBtn'); pausebtn.style.visibility = 'visible'; const spng = document.getElementById('spinning'); if (this.innerHTML=='Spin It') { const sp = new SpinnerElement( {} ); sp.id = 'thisSpinner'; spng.innerHTML = ''; spng.appendChild(sp); sp.setAttributes({color: 'turquoise', suffix: ' Spinning', rstyle: 'double', role: 'status', 'aria-wrap': 'true' }); this.innerHTML='Stop It'; } else { spng.innerHTML = '   '; pausebtn.style.visibility = 'hidden'; this.innerHTML='Spin It'; } ">Spin It</button>   <button type="button" id="pauseBtn" style="visibility:hidden;width:6em;" onclick=" const sp = document.getElementById('thisSpinner'); if (!sp) {return}; if (this.innerHTML == 'Pause') { sp.setSuffix(' Paused') sp.pause(); this.innerHTML = 'Resume'; } else { sp.setSuffix(' Spinning '); sp.go(); this.innerHTML = 'Pause'; }">Pause</button>     <span id="spinning">   </span> </p>

<p id="pace" style="font-size:2em;"> <button type="button" onclick=" if (this.innerHTML=='Pace Me') { this.currentSpeedIdx = 0; this.innerHTML = 'Step'; } const speeds = [ '0.1', '0.25', '0.5', '0.75', '1', '1.5', '2', '4', 'Reset' ]; if (getSpinner('#sp2')) { removeSpinner('#sp2') } const sp2 = new SpinnerElement({id: 'sp2'}); appendSpinner(sp2, '#pace'); let curSpd = speeds[this.currentSpeedIdx]; if (curSpd === 'Reset') { this.innerHTML = 'Pace Me'; removeSpinner('#sp2'); this.currentSpeedIdx = 0; } else { let ss = curSpd === '1' ? '' : 's'; sp2.setAttributes({'color': 'blue', 'speed': curSpd, kern: '1ch', 'prefix': 'Rotation in', 'suffix': curSpd + ' second' + ss}); this.currentSpeedIdx += 1; } ">Pace Me</button> </p>
<button type="button" role='alert' style="font-weight:bold;height:1.6em;width:12ch;font-size:1.4em" onclick=" if (this.innerHTML=='Run') { const sp = new SpinnerElement; sp.id = 'thatSpinner'; this.innerHTML = ''; this.appendChild(sp); sp.setAttributes({'color': 'purple', 'prefix': 'Cancel '}); } else { this.innerHTML='Run'; } ">Run</button>
Click to View
Layout: External Functions and Spinner Methods:
insertSpinner(sp1,'#here1'); removeSpinner( [ sp1 | '#sp1_id' ] );
appendSpinner(sp2,'#here2'); removeSpinner( [ sp2 | '#sp2_id' ] );
sp3.show(); sp3.hide();
sp4.unveil(); sp4.veil();
sp5.run(); sp5.stop();

Rotor Styles








All of the spinners above use the same basic spinner, with font-size flexing from 2em to 3em set in the page's style sheet, with the rotor color green inherited from the containing <div>, and the spinner's own markup specifying the light blue trace color. The table shows how the spinner varies with different rotor types and weights, and the buttons above change the rotor style for the whole set of spinners, which are identified in the HTML markup by the class "demoSpnr".

Here is the spinner markup for one of the spinners above: <div style="color:green;"> <x-spinner class="demoSpnr" rtr="101" wt=".195" tclr="rgba(0,55,255,.25)"></x-spinner> </div> yielding: , which still has the class "demoSpnr" so it has the size of the spinners above, and it will change rotor style when you click the buttons above.

Here is one of the buttons that sets the style of the rotors assigned the "demoSpnr" class, using one of the spinner's built-in methods: <button type="button" onclick=" const targetSpinners = document.querySelectorAll('.demoSpnr'); targetSpinners.forEach(spinner => { spinner.setRotor('dotted'); }); ">Dotted</button>

How is this spinner made?

These images illustrate how the rotor - the spinning part - is made from a square box HTML element with a border. One or more of the top, right, bottom, and left, borders are colored differently from the rest. Then the sides of the square are made round by giving the corners of the square a radius of 50% of its size. The result is four "quadrants" of the spinner to color and format for different spinners.

The faint background of the borders is called the trace. It may also be any color, or transparent so it doesn't show at all.

The spinning motion of the rotor is accomplished with the spinner's built-in css animation, rotating the (rounded) square 360° in the number of seconds specified with speed="[#]", rotating clockwise or counter-clockwise as specified with direction="[ cw | ccw ]".

You Can Make Your Own Base Spinner
With Customized Defaults!

The defaults for spinners may be customized, baking in your preferred attributes so it's not necessary to provide them in markup. Unless you specify otherwise, they will still assume the color and size of the text they're embedded in, like a text character .

Here is the standard spinner → , marked up with no added attributes as <x-spinner></x-spinner>

This page includes a script that uses the createSpinnerElement constructor provided by spinnerComponent.js to create a second spinner with a faster default speed and reverse direction: <script> createSpinnerElement('x-fast-revspinner', { speed: '.5', dir: 'ccw'}); </script>

Here is the second spinner → , marked up with no added attributes as <x-fast-revspinner></x-fast-revspinner>

A few of the spinners in the big table above are this type. Do you see them?

This page has another script to create a third "proprietary" spinner for the Acme company: <script> createSpinnerElement('acme-spnr-06', { sp: '.33', wt: '6', rtr: '101', dir: 'cw' }); </script>

The third spinner comes out like → , marked up with no added attributes as <acme-spnr-06></acme-spnr-06>

Naming custom spinners:

Note that the above scripts using `createSpinnerElement` add new spinner elements to the DOM; the original spinner with the tag name 'x-spinner' is still available.

Special Case: A Workaround for List Bullets

HTML list elements do not accept custom web components as bullet markers. Instead, the spinning bullets on the list above are provided by removing the list's own item markers by styling them list-style: none;, and then inserting the custom spinner elements at the very start of each list item, just before its content. The insertion is accomplished with a brief script triggered after full DOM load to ensure the list is all there. <script> document.addEventListener('DOMContentLoaded', function() { const listItems = document.querySelectorAll('.spin1 li'); const spinOpts = { rtr: '101', color: 'red', bgclr: 'gold', tclr: 'blue' }; listItems.forEach(function(item) { const spinner = new SpinnerElement( spinOpts ); item.insertAdjacentElement('afterbegin', spinner); }) }); </script>


Enabling the HTML SpinnerElement is as simple as adding the script spinnerComponent.js to your website and loading it as the "src" of an HTML <script> element: <script src="./scripts/spinnerComponent.js"></script>

The best source for the latest production release of spinnerComponent.js is from my account at GitHub.com. That release comes with a README file with documentation and also a copy of this page's HTML file you can inspect for examples.

<p> <x-spinner rstyle="double" rotor="101" style="font-size:60em;color:purple;margin:auto;"></x-spinner> </p>