Tucano v0.37.2

Loading

A spinner for an action someone triggered and is waiting on, like the save button; a skeleton for content that is still on its way and already has a known shape, like a table row. The skeleton prevents the jump: the space is already reserved at the size of what is coming. Classes only, no JavaScript.

Spinner

Click save: the button stays busy for two seconds.

Skeleton

The shape of what is coming, in the place where it will land.

Examples

The spinner is sized in em and painted with currentColor, so it follows wherever it sits without any variant.

Busy button

aria-busy="true" with disabled. Inside the button, the spinner takes the place and the size of the icon.

Inline with text

At the height of the letters, and in their color.

Fetching invoices…

Table row

The skeleton can be any element, including a <td>.

CustomerAmount

Avatar and card

is-circle turns round at whatever width you give it.

How to use

<button class="tuc-btn is-primary" aria-busy="true" disabled>
  <span class="tuc-spinner"></span> Saving
</button>

<span class="tuc-spinner is-lg"></span>

<div class="tuc-skeleton" style="width:60%"></div>
<span class="tuc-skeleton is-circle" style="width:36px"></span>

The skeleton is a block 1em tall and full width: the width and, when needed, the height are up to you. Don't hand-roll a spinner with your own SVG or border — the loading toast and the components use this one.

Busy is not disabled

disabled prevents the second click, but the 50% fade says "not available", and the button is working. With aria-busy="true" it keeps its full color and gets the wait cursor.

With HTMX

htmx-indicator is HTMX's own class: hidden, and visible only during the request. Paired with the spinner, it shows up and goes away without a single line of JavaScript.

<button class="tuc-btn is-primary" hx-post="{% url 'contract-save' contract.pk %}" hx-disabled-elt="this">
  <span class="tuc-spinner htmx-indicator"></span> Save
</button>

The skeleton works as placeholder content for what HTMX fetches as soon as the page opens. When the response arrives, it replaces the skeleton in the same space, and nothing jumps.

<div hx-get="{% url 'customers-table' %}" hx-trigger="load" hx-swap="outerHTML">
  <span class="tuc-skeleton" style="width:60%"></span>
  <span class="tuc-skeleton" style="width:80%; margin-top:8px"></span>
</div>

Accessibility

The text says what is happening, and the spinner confirms it. "Saving" next to the spinner gets announced; a spinner on its own is just a spinning circle to someone who can't see it.

The skeleton is decoration: it doesn't select text, doesn't take the pointer, and its text, if any, is transparent. With prefers-reduced-motion the skeleton's shimmer stops entirely, because it is only ornament. The spinner keeps spinning, more slowly: standing still, it would stop saying "waiting" and become just another circle.

Classes

Class or attributeWhat for
.tuc-spinnerSpinning circle, 1em, in the text color
is-lgOn the spinner: 1.5em
.tuc-btn > .tuc-spinnerInside the button, at the size of its icon
aria-busy="true"On .tuc-btn: busy, with full color and the wait cursor, even with disabled
.tuc-skeletonShimmering block, in place of the content that is coming
is-circleOn the skeleton: round, with height equal to width
htmx-indicatorFrom HTMX: shows the element only during the request