# Spinner (/ui/components/react/spinner)



<Playground component="spinner" />

## Usage [#usage]

```tsx
import { Spinner } from '@appica/ui-react/spinner'
```

```tsx
<Spinner />
```

`Spinner` is a small, self-contained loading indicator that animates forever — use it whenever something is happening and you can't show progress. It comes in three shapes via `variant`: `circular` (the default sweep), `dots` (a ring of fading ticks), and `sparkle` (a morphing four-point star).

Two things make it drop into anything: it's **sized by font-size**, so set a `text-*` class (or inherit `1em` from a parent) to scale it, and `currentColor` makes it take the surrounding text color instead of the primary accent — which is exactly what you want inside a colored button. It renders a `role="status"` element with an `aria-label` of "Loading".

Reach for `Spinner` for compact, in-place waits — a button, an icon button, a small overlay. For a wider "this section is loading" indicator that reads more like progress, use [`Loader`](/ui/components/react/loader).

## Examples [#examples]

### Variants [#variants]

Three shapes for different personalities: `circular`, `dots`, and `sparkle`. All animate indefinitely and honour `prefers-reduced-motion` (they hold a static frame instead of spinning).

```tsx
import { Spinner } from '@appica/ui-react/spinner'

const VARIANTS = ['circular', 'dots', 'sparkle'] as const

export default function SpinnerVariants() {
  return (
    <div className="flex items-center gap-10">
      {VARIANTS.map((variant) => (
        <div key={variant} className="flex flex-col items-center gap-3">
          <Spinner variant={variant} />
          <span className="text-foreground-muted text-xs">{variant}</span>
        </div>
      ))}
    </div>
  )
}
```

### Sizing [#sizing]

The spinner is sized by `font-size`, so a `text-*` utility scales the whole thing — no `size` prop. Drop one next to text with no class and it inherits `1em`, matching the line.

```tsx
import { Spinner } from '@appica/ui-react/spinner'

export default function SpinnerSizing() {
  return (
    <div className="flex items-end gap-8">
      <Spinner className="text-xl" />
      <Spinner className="text-3xl" />
      <Spinner className="text-5xl" />
      <Spinner className="text-7xl" />
    </div>
  )
}
```

### Color [#color]

By default the spinner uses the primary accent. Add `currentColor` and it inherits the surrounding text color instead — ideal on colored surfaces or to match a status color.

```tsx
import { Spinner } from '@appica/ui-react/spinner'

export default function SpinnerColor() {
  return (
    <div className="flex items-center gap-8 text-4xl">
      <Spinner />
      <span className="text-violet-500">
        <Spinner currentColor />
      </span>
      <span className="text-success-emphasis">
        <Spinner currentColor />
      </span>
      <span className="text-error-emphasis">
        <Spinner currentColor />
      </span>
    </div>
  )
}
```

### In a button [#in-a-button]

The most common use: a submit button that shows a spinner while the action runs. Set `currentColor` so it matches the button's text, size it with `text-[1.2em]` so it scales with the button, and disable the button while loading. The second button pairs the `sparkle` variant with a [`Gradient Glow`](/ui/components/react/gradient-glow) for an "Ask AI" affordance that lights up while it thinks.

```tsx
'use client'

import * as React from 'react'
import { Button, buttonVariants } from '@appica/ui-react/button'
import { Spinner } from '@appica/ui-react/spinner'
import { GradientGlow } from '@appica/ui-react/gradient-glow'
import { Sparkle } from '@appica/icons-react'
import { cn } from '@/lib/utils'

export default function SpinnerButton() {
  const [saving, setSaving] = React.useState(false)
  const [thinking, setThinking] = React.useState(false)
  const saveTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
  const askTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  const save = () => {
    if (saveTimer.current) clearTimeout(saveTimer.current)
    setSaving(true)
    saveTimer.current = setTimeout(() => setSaving(false), 1800)
  }

  const ask = () => {
    if (askTimer.current) clearTimeout(askTimer.current)
    setThinking(true)
    askTimer.current = setTimeout(() => setThinking(false), 5000)
  }

  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      <Button onClick={save} disabled={saving} className="min-w-34">
        {saving && <Spinner currentColor className="text-[1.2em]" />}
        {saving ? 'Saving…' : 'Save changes'}
      </Button>

      <GradientGlow reveal={thinking} border blur="md" speed={3} className="rounded-full">
        <button
          type="button"
          onClick={ask}
          className={cn(buttonVariants({ variant: 'outline', size: 'md' }), 'rounded-full')}
        >
          {thinking ? (
            <Spinner variant="sparkle" currentColor data-icon="start" className="text-lg" />
          ) : (
            <Sparkle data-icon="start" />
          )}
          {thinking ? 'Thinking…' : 'Ask AI'}
        </button>
      </GradientGlow>
    </div>
  )
}
```

### Refresh button [#refresh-button]

In an icon button, swap the icon for a spinner while the action is in flight — the button keeps its size, so the layout never jumps.

```tsx
'use client'

import * as React from 'react'
import { Button } from '@appica/ui-react/button'
import { Spinner } from '@appica/ui-react/spinner'
import { Refresh } from '@appica/icons-react'

export default function SpinnerRefresh() {
  const [loading, setLoading] = React.useState(false)

  const refresh = () => {
    setLoading(true)
    setTimeout(() => setLoading(false), 1500)
  }

  return (
    <Button variant="outline" size="icon-md" aria-label="Refresh data" disabled={loading} onClick={refresh}>
      {loading ? <Spinner variant="dots" currentColor className="text-[1.25em]" /> : <Refresh />}
    </Button>
  )
}
```

### Loading a panel [#loading-a-panel]

Drive a region's loading state from your own state: show a centred spinner while fetching, then swap in the content. Here a status card reloads on demand.

```tsx
'use client'

import * as React from 'react'
import { Button } from '@appica/ui-react/button'
import { Spinner } from '@appica/ui-react/spinner'
import { Refresh } from '@appica/icons-react'

export default function SpinnerPanel() {
  const [loading, setLoading] = React.useState(true)

  const reload = React.useCallback(() => {
    setLoading(true)
    const timer = setTimeout(() => setLoading(false), 1600)
    return () => clearTimeout(timer)
  }, [])

  React.useEffect(() => reload(), [reload])

  return (
    <div className="border-border bg-background w-72 rounded-xl border p-5">
      {loading ? (
        <div className="flex h-28 items-center justify-center">
          <Spinner variant="circular" className="text-4xl" />
        </div>
      ) : (
        <div className="flex h-28 flex-col justify-between">
          <div>
            <p className="text-foreground-intense font-semibold">Production</p>
            <p className="text-success-emphasis text-sm">All systems operational</p>
            <p className="text-foreground-muted mt-1 text-xs">Synced just now</p>
          </div>
          <Button variant="soft" size="sm" className="self-start" onClick={reload}>
            <Refresh data-icon="start" />
            Reload
          </Button>
        </div>
      )}
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The spinner itself is direction-agnostic; inside a button it simply leads the label, which sits on the correct side under RTL. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="spinner" />

## API reference [#api-reference]

`Spinner` renders a single `<span role="status">` and forwards `ref`, `className`, and any other span attributes.

| Prop           | <ColMinWidth width="210">Type</ColMinWidth>    | Default      | <ColMinWidth width="240">Description</ColMinWidth>                                                          |
| -------------- | ---------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------- |
| `variant`      | <Code>'circular' \| 'dots' \| 'sparkle'</Code> | `'circular'` | The animated shape.                                                                                         |
| `currentColor` | `boolean`                                      | `false`      | Inherit the surrounding text color (`currentColor`) instead of the primary accent.                          |
| `aria-label`   | `string`                                       | `'Loading'`  | Accessible name for the `role="status"` region.                                                             |
| `className`    | `string`                                       | —            | Extra classes, merged via `tailwind-merge`. Set a `text-*` size here — the spinner scales with `font-size`. |

## Accessibility [#accessibility]

* The spinner is a `role="status"` live region with a default `aria-label="Loading"`; give it a more specific label (e.g. "Saving") when the context warrants it.
* Inside a button, the button's own text ("Saving…") is the accessible name — the spinner just reinforces it visually.
* A purely decorative spinner that sits beside text already conveying the state can be hidden from assistive tech with `aria-hidden`.
* The animation is replaced by a static frame when the user prefers reduced motion.
