Loading indicators
The one piece of design the library ships, and how to replace it.
The exception to unstyled
Panel and PanelSet ship no styling, with one exception: the loading indicator. While async/remote/slow content loads, you get a spinner, and it has a size, a thickness, a speed and colors.
The spinner is there because the library needs it. Panel and PanelSet need to show at least something to indicate to the user that there is some waiting to be expected. That "at least something" is the spinner. You do not have to use it, but you do have to show something. NOT showing anything if there is async/remote/slow content loading in the background is worse.
You will probably want one of three things, and they are the three sections below:
- The default spinner, in the panel area. You get it out of the box.
- Your own spinner, with your own size, thickness, speed and colors. You can do that with CSS variables.
- Your own indicator: a spinner in the trigger, a progress bar, skeleton content, a label change. You can do that with the state the library exposes.
The library supplies the state, not the look. .is-loading on the panel, aria-busy and .is-trigger-loading on the trigger. The spinner is only the default way of showing that state. Replace it and nothing else changes.
1. The default spinner
Nothing to set up. Load content asynchronously and the spinner appears in the panel area, in both components:
Panel
PanelSet
Key Features
- This is our most important feature ever!
It only appears if the load takes longer than loadingDelay (320ms by default), so a fast load opens straight to its content without a flash of spinner. While it shows, the panel holds loadingHeight (150px by default) so there is somewhere for it to live, and the old content dims behind it.
new Panel('#my-panel', {
loadingDelay: 320, // wait this long before showing anything
loadingHeight: 150 // the room it holds while it waits
});
2. Your own spinner
Same spinner, your values. Every part of it is a CSS variable, so nothing needs to be rebuilt. The pair below is the demo above with five variables changed:
Panel
PanelSet
Key Features
- This is our most important feature ever!
.my-panel {
--ps-spinner-size: 24px;
--ps-spinner-thickness: 8px;
--ps-spinner-track-color: pink;
--ps-spinner-color: limegreen;
--ps-spinner-speed: 1.4s;
}
Set them wherever they inherit from: on :root for every panel on the page, or on one element to change just that one.
CSS variables
- --ps-spinner-size
- How big the circle is. Default
40px. - --ps-spinner-thickness
- How thick the ring is. Default
4px. - --ps-spinner-track-color
- The ring that stays put. Default
rgba(0, 0, 0, 0.1). - --ps-spinner-color
- The arc that travels, which is what reads as movement. Default
rgba(0, 100, 255, 0.8). - --ps-spinner-speed
- One full turn. Default
0.8s. - --ps-spinner-fade-in-speed
- How long it takes to fade up once it appears, so it does not arrive as a flash. it is unset by default, which means it follows whatever the spinner appears alongside: the content dimming in a PanelSet (
--ps-loading-dim-duration), or the panel opening in a Panel (--ps-open-speed). Give it a value to pick one number for both. - --ps-spinner-fade-out-speed
- How long it takes to fade away once the content is there. Default
0.2s. Separate from the one above, so the spinner can come up gently and leave quickly. - --ps-spinner-top / --ps-spinner-bottom
- Defaults:
50%andauto. - --ps-spinner-left / --ps-spinner-right
- Defaults:
50%andauto. - --ps-spinner-transform
- Default
translate(-50%, -50%), which pulls the circle back by half its own size so that50%/50%is its middle rather than its top-left corner. To place it against an edge instead, set this totranslate(0, 0). It has to stay a transform function:noneis not valid here, because the spin is composed on top of it. - --ps-loading-panel-opacity
- How far the old content fades back while the new content loads. Default
0.1. Set it to1to leave the content alone. - --ps-loading-dim-duration
- How long that fade back takes (PanelSet). Follows
--ps-panel-out-speedunless you set it, and the spinner fades in over the same time, so the two read as one cross-fade.
3. Your own indicator
If a spinner in the panel area is not what you want at all, take it out and show the wait yourself. In the pair below the spinner sits in the trigger instead, the button of the Panel and the tab of the PanelSet, and the content area holds no room for one:
Panel
PanelSet
Key Features
- This is our most important feature ever!
Two things make that work:
customIndicatortells the library you are showing the state yourself, so it does not show its spinner.loadingHeight: 0is the other half: with the indicator on the trigger, the content area does not need to hold space for one, so it stays collapsed until the real content arrives. That is why the demos above open straight to their content.
new Panel('#my-panel', {
customIndicator: true,
loadingHeight: 0
});
new PanelSet('#my-panelset', {
customIndicator: true,
loadingHeight: 0
});
Or use it as one-offs, as data-attributes:
<div data-panel data-panel-custom-indicator data-panel-loading-height="0"> … </div>
<div data-panelset data-panel-custom-indicator data-loading-height="0"> … </div>
After that, you can use the following states and classes for styling:
- .is-loading
- On the panel or the panelset, while its content loads.
- aria-busy="true"
- On every trigger pointing at that panel. This is the real accessibility signal for a control whose content is being fetched, so it is worth keying off rather than the class.
- .is-trigger-loading
- On those same triggers, for when you want a class instead.
/* show it on the trigger: a button, or a tab */
[aria-busy="true"]::after {
content: "";
display: inline-block;
width: 14px;
height: 14px;
margin-left: 0.5em;
border: 2px solid rgb(0 0 0 / 0.15);
border-top-color: currentColor;
border-radius: 50%;
animation: my-spin 0.6s linear infinite;
}
@keyframes my-spin {
to { rotate: 360deg; }
}
Both components mark that state while content loads, and clear it the moment the load settles, whether it finishes, is cancelled, or fails.
It does not have to be a spinner. The state is just a state: fade the trigger, put a progress bar above the panel, show skeleton content, swap the label. The library does not care what you show.
Pricing Plans
- Starter Plan: Whenever you don’t want to pay a lot.
- Professional Plan: Ideal for managers who like to spend money!