Indicates a loading state with an animated visual cue.
import { Spinner } from 'frontile';
import { Spinner } from 'frontile';
<template><Spinner /></template>
@size controls the Spinner's size: xs, sm, md, lg, or xl.
import { Spinner } from 'frontile';
<template>
<div class='flex items-center space-x-2'>
<Spinner @size='xs' />
<Spinner @size='sm' />
<Spinner @size='md' />
<Spinner @size='lg' />
<Spinner @size='xl' />
</div>
</template>
@color changes the Spinner's color to match common UI patterns, such
as primary actions, success states, warnings, and danger actions.
import { Spinner } from 'frontile';
<template>
<div class='flex items-center space-x-2'>
<Spinner @color='neutral' />
<Spinner @color='primary' />
<Spinner @color='secondary' />
<Spinner @color='tertiary' />
<Spinner @color='success' />
<Spinner @color='warning' />
<Spinner @color='danger' />
</div>
</template>
Add custom CSS classes with the class argument for anything the built-in
options don't cover. Override colors with fill-{*} classes for the
highlighted color and text-{*} classes for the background. Semantic color
utilities adapt automatically across themes.
import { Spinner } from 'frontile';
<template>
<div class='flex items-center justify-center'>
<Spinner @class='h-24 w-24 fill-secondary text-primary-muted' />
</div>
</template>
The spinner is aria-hidden by default: it is a picture of a state, not the
state itself. An unhidden, unnamed <svg> is announced as an
image with no name, which tells a screen reader user nothing — and naming the
graphic ("Loading spinner") describes the decoration rather than saying that the
content they asked for is on its way.
Put the state on the thing that is loading. aria-busy marks the region, and a
polite live region announces the transition once:
No routes loaded yet.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Button, Spinner } from 'frontile';
export default class Example extends Component {
@tracked isLoading = false;
@tracked rows: string[] = [];
load = async () => {
this.isLoading = true;
this.rows = [];
await new Promise((resolve) => setTimeout(resolve, 1500));
this.rows = ['Fiber route A', 'Fiber route B', 'Fiber route C'];
this.isLoading = false;
};
<template>
<div class='flex flex-col gap-3'>
<Button @color='primary' @onPress={{this.load}}>Load routes</Button>
<div
aria-busy={{if this.isLoading 'true' 'false'}}
aria-live='polite'
class='border-neutral-soft rounded border p-4'
>
{{#if this.isLoading}}
<span class='flex items-center gap-2'>
<Spinner @size='sm' @color='primary' />
Loading routes…
</span>
{{else if this.rows}}
<ul class='not-prose'>
{{#each this.rows as |row|}}
<li>{{row}}</li>
{{/each}}
</ul>
{{else}}
<p class='text-neutral'>No routes loaded yet.</p>
{{/if}}
</div>
</div>
</template>
}
The visible "Loading routes…" text is what carries the meaning here — the spinner beside it is redundant by design, which is exactly what makes hiding it correct.
A spinner with no adjacent text needs the text supplied some other way, or the
wait is silent. VisuallyHidden is the usual answer for a button that swaps its
label for a spinner:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Button, Spinner, VisuallyHidden } from 'frontile';
export default class Example extends Component {
@tracked isSaving = false;
save = async () => {
this.isSaving = true;
await new Promise((resolve) => setTimeout(resolve, 1500));
this.isSaving = false;
};
<template>
<Button @color='primary' @onPress={{this.save}} disabled={{this.isSaving}}>
{{#if this.isSaving}}
<Spinner @size='sm' />
<VisuallyHidden>Saving, please wait</VisuallyHidden>
{{else}}
Save
{{/if}}
</Button>
</template>
}
If you need the graphic itself announced — a full-page loader with nothing else on screen — pass your own attributes, which are applied after the default and so win:
import { Spinner } from 'frontile';
<template>
<Spinner @size='xl' aria-hidden='false' role='status' aria-label='Loading' />
</template>
Motion is the other consideration: the theme applies animate-spin with no
motion-reduce variant, so the spinner keeps turning for users who have asked
for reduced motion. If your product treats that preference as absolute, suppress
it yourself:
import { Spinner } from 'frontile';
<template>
<Spinner @color='primary' @class='motion-reduce:animate-none' />
</template>
Which is another reason not to let a spinner be the only sign that something is happening: the text beside it keeps working when the animation does not.
Element: SVGElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- |
Custom class name, it will override the default ones using Tailwind Merge
library. Use fill-* for the highlighted arc and text-* for the track.
|
color
|
enum
|
'neutral'
|
The color of the spinner. |
intent
Deprecated
|
enum
|
- |
Deprecated. Use `color`. `default` is now `neutral`. |
size
|
enum
|
'md'
|
The size of the spinner. |