# Spinner

An indicator that shows a loading state.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"
import { color, leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  row: {
    width: "100%",
    maxWidth: "20rem",
    borderRadius: radius.xl,
    backgroundColor: color.muted,
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  amount: { marginInlineStart: "auto", fontVariantNumeric: "tabular-nums" },
})

export default function SpinnerDemo() {
  return (
    <HStack align="center" gap={3} padding={4} xstyle={styles.row}>
      <Spinner />
      <span>Processing payment...</span>
      <span {...stylex.props(styles.amount)}>$100.00</span>
    </HStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation).

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/spinner.tsx"
   "use client"

   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { Loader2 } from "lucide-react"

   import { radius, space } from "@/theme/tokens.stylex"

   const spin = stylex.keyframes({ to: { transform: "rotate(360deg)" } })

   const blink = stylex.keyframes({
     "0%": { opacity: 0.25, transform: "scale(0.8)" },
     "40%": { opacity: 1, transform: "scale(1)" },
     "80%": { opacity: 0.25, transform: "scale(0.8)" },
     "100%": { opacity: 0.25, transform: "scale(0.8)" },
   })

   const styles = stylex.create({
     base: {
       width: `var(--carbon-icon-size, ${space.s4})`,
       height: `var(--carbon-icon-size, ${space.s4})`,
       flexShrink: 0,
       animationName: spin,
       animationDuration: "1s",
       animationTimingFunction: "linear",
       animationIterationCount: "infinite",
     },
     ring: {
       display: "inline-block",
       borderRadius: radius.full,
       backgroundImage: "conic-gradient(from 0deg, transparent 15%, currentColor)",
       maskImage: "radial-gradient(farthest-side, transparent 72%, black 75%)",
       animationDuration: "0.8s",
     },
     dots: {
       display: "inline-flex",
       alignItems: "center",
       justifyContent: "space-between",
       animationName: "none",
     },
     dot: {
       width: "26%",
       height: "26%",
       borderRadius: radius.full,
       backgroundColor: "currentColor",
       animationName: blink,
       animationDuration: "1.2s",
       animationTimingFunction: "ease-in-out",
       animationIterationCount: "infinite",
     },
     dot2: { animationDelay: "0.15s" },
     dot3: { animationDelay: "0.3s" },
   })

   const variants = { default: null, ring: styles.ring, dots: styles.dots }

   const spinnerStyles = {
     base: styles.base,
     variants,
     dot: styles.dot,
     dotDelays: [null, styles.dot2, styles.dot3],
   }

   type SpinnerVariant = keyof typeof variants

   type SpinnerProps = useRender.ComponentProps<"svg"> & {
     variant?: SpinnerVariant
     xstyle?: StyleXStyles
     className?: string
   }

   function Spinner({
     variant = "default",
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: SpinnerProps) {
     const sx = stylex.props(styles.base, variants[variant], xstyle)
     const glyph = variant === "default"

     return useRender({
       defaultTagName: glyph ? "svg" : "span",
       render: render ?? (glyph ? <Loader2 /> : undefined),
       ref,
       props: {
         "data-slot": "spinner",
         "data-variant": variant,
         role: "status",
         "aria-label": "Loading",
         ...(variant === "dots" && {
           children: spinnerStyles.dotDelays.map((delay, index) => (
             <span key={index} {...stylex.props(styles.dot, delay)} />
           )),
         }),
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   export { Spinner, spinnerStyles, type SpinnerVariant }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Spinner } from "@/components/ui/spinner"
```

```tsx
<Spinner />
```

The spinner keeps turning under `prefers-reduced-motion: reduce`, because a
stopped spinner looks like a stalled one.

## Variants

`default` turns a glyph. `ring` is a ring with one bright head and a tail that
fades behind it. `dots` lights three dots one after another. All three take the
same size and colour, and all keep moving under reduced motion.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  spinner: { width: space.s6, height: space.s6 },
})

export default function SpinnerVariants() {
  return (
    <HStack align="center" gap={8}>
      <Spinner xstyle={styles.spinner} />
      <Spinner variant="ring" xstyle={styles.spinner} />
      <Spinner variant="dots" xstyle={styles.spinner} />
    </HStack>
  )
}
```

## Size

A spinner takes its size from `--carbon-icon-size`, the property a control sets
for its icons, so a spinner inside a small button or a badge shrinks with the
icons beside it. Give a standalone spinner a `width` and `height` through
`xstyle`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  xs: { width: space.s3, height: space.s3 },
  sm: { width: space.s4, height: space.s4 },
  md: { width: space.s6, height: space.s6 },
  lg: { width: space.s8, height: space.s8 },
})

export default function SpinnerSize() {
  return (
    <HStack align="center" gap={6}>
      <Spinner xstyle={styles.xs} />
      <Spinner xstyle={styles.sm} />
      <Spinner xstyle={styles.md} />
      <Spinner xstyle={styles.lg} />
    </HStack>
  )
}
```

## Custom icon

Spread `spinnerStyles.base` onto any icon to give it the spinner's size and
rotation. Add `role="status"` and `aria-label` yourself; only `Spinner` sets
them for you.

```tsx
import * as stylex from "@stylexjs/stylex"
import { spinnerStyles } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"
import { Loader, LoaderPinwheel, RefreshCw } from "lucide-react"

export default function SpinnerCustom() {
  return (
    <HStack align="center" gap={6}>
      <Loader role="status" aria-label="Loading" {...stylex.props(spinnerStyles.base)} />
      <LoaderPinwheel role="status" aria-label="Loading" {...stylex.props(spinnerStyles.base)} />
      <RefreshCw role="status" aria-label="Loading" {...stylex.props(spinnerStyles.base)} />
    </HStack>
  )
}
```

## Button

Place the spinner before the label with `iconPosition="start"`, or after it
with `iconPosition="end"`.

```tsx
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"

export default function SpinnerButton() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Button size="sm" disabled iconPosition="start">
        <Spinner aria-hidden="true" />
        Loading...
      </Button>
      <Button variant="outline" size="sm" disabled iconPosition="start">
        <Spinner aria-hidden="true" />
        Please wait
      </Button>
      <Button variant="secondary" size="sm" disabled iconPosition="end">
        Processing
        <Spinner aria-hidden="true" />
      </Button>
    </HStack>
  )
}
```

## Badge

Place the spinner before the label with `iconPosition="start"`, or after it
with `iconPosition="end"`.

```tsx
import { Badge } from "@/components/ui/badge"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"

export default function SpinnerBadge() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge iconPosition="start">
        <Spinner aria-hidden="true" />
        Syncing
      </Badge>
      <Badge variant="secondary" iconPosition="start">
        <Spinner aria-hidden="true" />
        Updating
      </Badge>
      <Badge variant="outline" iconPosition="end">
        Processing
        <Spinner aria-hidden="true" />
      </Badge>
    </HStack>
  )
}
```

## Input group

Put the spinner in an `InputGroupAddon` and position it with the addon's
`align`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText } from "@/components/ui/input-group"
import { Spinner } from "@/components/ui/spinner"
import { VStack } from "@/components/ui/stack"
import { Textarea, textareaStyles } from "@/components/ui/textarea"
import { ArrowUp } from "lucide-react"

const styles = stylex.create({
  stack: { width: "100%", maxWidth: "24rem" },
  send: { marginInlineStart: "auto" },
})

export default function SpinnerInputGroup() {
  return (
    <VStack gap={4} xstyle={styles.stack}>
      <InputGroup disabled>
        <Input placeholder="Send a message" aria-label="Message" />
        <InputGroupAddon align="inline-end">
          <Spinner />
        </InputGroupAddon>
      </InputGroup>
      <InputGroup disabled>
        <Textarea
          placeholder="Send a message"
          aria-label="Message"
          data-slot="input-group-control"
          xstyle={textareaStyles.inGroup}
        />
        <InputGroupAddon align="block-end">
          <Spinner aria-hidden="true" />
          <InputGroupText>Validating...</InputGroupText>
          <InputGroupButton variant="default" size="icon-xs" aria-label="Send" xstyle={styles.send}>
            <ArrowUp />
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
    </VStack>
  )
}
```

> **Note**
>
> `Spinner` renders `role="status"` with `aria-label="Loading"`, so a screen
> reader announces it on its own. Pass `aria-hidden` when the text beside it
> already says the same thing, such as a button labelled "Loading...".

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Spinner` renders an `<svg>` for `default` and a `<span>` for `ring` and
`dots`, and accepts every attribute of its element. Anything you pass overrides
its own defaults, including `role` and `aria-label`.

### Spinner

| Prop         | Type                            | Default     |
| ------------ | ------------------------------- | ----------- |
| `variant`    | `"default" \| "ring" \| "dots"` | `"default"` |
| `xstyle`     | `StyleXStyles`                  | —           |
| `className`  | `string`                        | —           |
| `role`       | `string`                        | `"status"`  |
| `aria-label` | `string`                        | `"Loading"` |

### spinnerStyles

The style map behind the component. Spread it onto any element.

| Key         | What it is                                   |
| ----------- | -------------------------------------------- |
| `base`      | The inherited icon size and the rotation     |
| `variants`  | The ring's mask and the dots' row, by name   |
| `dot`       | One dot and its blink                        |
| `dotDelays` | The second and third dots' offsets, in order |

### Data attributes

| Attribute             | On          | Effect                          |
| --------------------- | ----------- | ------------------------------- |
| `data-slot="spinner"` | the spinner | Targets the glyph from a parent |
| `data-variant`        | the spinner | Mirrors the `variant` prop      |
