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:
spinnerComponent.js
(see Sources, below)
./scripts
directory
<script src="./scripts/spinnerComponent.js"></script>
<x-spinner></x-spinner>
<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.
Once created, a specific spinner may be saved as plain HTML and CSS, needing no further Javascript. If that is the only spinner for the page/site, loading spinnerComponent.js is not necessary. The saved spinner is a small HTML fragment composed of 'div' and 'span' elements, along with a 'style' element with properties that control the spinner's appearance and rotation.
Any spinner may be saved, either by stringifying it from a script, or by extracting it from the rendered html of the page it's created on. Some of the examples below put the stringified spinner in the web browser's console, where they can be copied. One example, the button "Add and View" adds a spinner and shows its stringified source.
The source of a dynamic spinner is normally hidden in the shadow DOM, and instances of the spinner may be varied in the same document by varying the attributes of the x-spinner element wherever it's placed. A saved spinner uses the inner HTML and CSS directly in the main DOM, removing the need for any Javascript, but also introducing potential HTML and CSS naming collisions, requiring any variations to be done directly in the spinner-related CSS, and, of course, removing its Javascript methods.
A saved spinner will still inherit the size and color of the text and markup it's placed in.
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.
<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>
<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 '<' instead of '<'
if (this.innerHTML == 'Search') {
let spn = `<x-spinner id='spinMe'></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>
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();
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:
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>
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 ]"
.
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 →
<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 →
<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 →
<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.
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.
You can simply embed the script from a cdn, jsdelivr.net, which taps my latest release on Github:
<script src="https://cdn.jsdelivr.net/gh/bwva/HTML-SpinnerElem/src/spinnerComponent.js"></script>
<p>
<x-spinner rstyle="double" rotor="101" style="font-size:60em;color:purple;margin:auto;"></x-spinner>
</p>