rapidUI

Spinner

Loading indicator.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/spinner.json

Installs to @/components/ui/spinner.

Manual installation

Install the dependencies:

pnpm add lucide-react@^1.48.0

Copy the source into your project:

components/ui/spinner.tsx
import { Loader2Icon } from 'lucide-react'
import { cn } from '@/lib/utils'

/**
 * An animated loading indicator, announced as a status named "Loading".
 *
 * Pass an `aria-label` to translate or specify the name: this package carries no translations, so "Loading" is only
 * an English fallback. Pass `aria-hidden` when visible text next to the spinner already says what is happening; the
 * spinner is then decorative and has no role or name.
 */
function Spinner({
	className,
	role = 'status',
	'aria-hidden': ariaHidden,
	'aria-label': ariaLabel,
	'aria-labelledby': ariaLabelledBy,
	...props
}: React.ComponentProps<'svg'>) {
	const hidden = ariaHidden === true || ariaHidden === 'true'

	return (
		<Loader2Icon
			data-slot="spinner"
			className={cn('size-4 animate-spin', className)}
			{...(hidden
				? { 'aria-hidden': true }
				: {
						role,
						'aria-label': ariaLabel ?? (ariaLabelledBy ? undefined : 'Loading'),
						'aria-labelledby': ariaLabelledBy,
					})}
			{...props}
		/>
	)
}

export { Spinner }

An indicator that shows a loading state. It renders an animated icon with role="status" and the accessible name "Loading", and can be sized and coloured with utility classes or placed inside buttons, badges and other components.

Features

  • Accessible by default with role="status" and aria-label="Loading"
  • aria-label replaces the English default with a translation or a more specific description
  • aria-hidden makes the spinner decorative, with no role or name, next to text that describes the state
  • Size and colour via className
  • Works inside buttons, badges, inputs and attachments

Usage

import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'

export function SubmittingButton() {
  return (
    <Button disabled>
      <Spinner aria-hidden />
      Submitting application
    </Button>
  )
}

Naming the spinner

rapidUI carries no translations, so "Loading" is only an English fallback. Pass the name from your app's translation layer with aria-label, or point at visible text with aria-labelledby:

// On its own: give it a translated or more specific name
<Spinner aria-label={t('cases.loading')} />

// Next to visible text that says the same thing: hide it from screen readers
<Button disabled>
  <Spinner aria-hidden />
  {t('application.sending')}
</Button>

With aria-hidden the spinner renders without role="status" and without a label, so screen readers skip it and read only the text.

Examples

Default

A background job shown as an Item, with a specific aria-label on the spinner.

Accessible Name

The default name, a translated aria-label, and aria-hidden next to visible text.

Sizes

Different sizes via size classes.

Colors

Different colours via theme text colour classes (text-primary, text-destructive, text-chart-2…).

In Button

Inside disabled buttons during submission, at the start or end with data-icon. The button text says what is happening, so the spinners are aria-hidden.

In Badge

Inside badges for background processes, aria-hidden next to the badge text.

In Input Group

As an inline-end addon while an address is looked up in the property register.

Empty State

Inside an Empty while records are retrieved from the archive, with a way to cancel.

Loading State

Per-file progress in an attachment list for a building permit application.

Accessibility

  • Has role="status" and aria-label="Loading" by default
  • Pass aria-label to translate the name or make it more specific, such as "Generating decision letter". aria-labelledby works too and replaces the default label
  • Pass aria-hidden when visible text next to the spinner describes the state, as in a button or badge. The spinner then has no role and no label, so the state isn't announced twice
  • Disable buttons while an action is in progress to prevent duplicate submissions
  • Respect reduced motion preferences where the animation is not essential