Skip to content
v2.5.0
English
Русский
Documentation

Spinner

Spinner is the loading indicator for pending states and asynchronous actions.
spinner.tsx · 3 lines
import { Spinner } from '@vega-ui/react';

<Spinner size={6} aria-label='Loading' />
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
11 tests

Summary

Spinner is the loading indicator for pending states and asynchronous actions. It is sized numerically and exposes primary or secondary color variants when the UI only needs compact indeterminate activity feedback.

Imports

import {
  Spinner,
  type SpinnerProps,
  type SpinnerSize,
  type SpinnerVariant,
} from '@vega-ui/react';

Exported Types

  • SpinnerProps
  • SpinnerSize
  • SpinnerVariant

Basic Usage

basic-usage.tsx · 5 lines
import { Spinner } from '@vega-ui/react';

export default function BasicUsage() {
  return <Spinner size={3} aria-label='Loading messages' />;
}

Variants

  • Size: 1 through 11
  • Variant: primary or secondary
  • Placement: inline, inside controls, or centered in surfaces

Edge Cases

  • A spinner without context can be ambiguous.
  • Oversized spinners inside compact controls hurt balance.
  • Loading indicators should preserve layout stability where possible.
  • If exact completion is known, a Progress bar usually communicates state better than a spinner.

Common Mistakes

  • Showing an isolated spinner without surrounding meaning.
  • Replacing too much content with a spinner when a skeleton or retained layout would be clearer.
  • Using the same size for button loading and page loading.

Examples

Basic

Basic: inline status with a visible label

The spinner takes its accessible name from the visible text next to it through aria-labelledby, so the text and the announced name never drift apart.

inline-status-row.tsx · 13 lines
import { Spinner, Text } from '@vega-ui/react';
import { useId } from 'react';

export default function InlineStatusRow() {
  const labelId = useId();

  return (
    <div style={{ display: 'flex', gap: 'var(--spacing-4)', alignItems: 'center' }}>
      <Spinner size={2} aria-labelledby={labelId} />
      <Text id={labelId} size={2}>Syncing 128 contacts from Google</Text>
    </div>
  );

Controlled/Stateful

Controlled/Stateful: loading a section on demand

A loading flag in state decides whether the section shows the spinner or the loaded list. The section is marked aria-busy while loading and keeps a minimum height, so the button and the content below do not jump.

conditional-async-state.tsx · 65 lines
import { Button, Spinner, Text } from '@vega-ui/react';
import { useEffect, useState } from 'react';

const activity = [
  'Maya Chen merged “Fix invoice rounding”',
  'Deploy #4182 finished in 3m 12s',
  'Omar Haddad commented on “Pricing page copy”',
];

export default function ConditionalAsyncState() {
  const [loading, setLoading] = useState(false);
  const [items, setItems] = useState<string[]>([]);

Form/Integration

Form/Integration: pending submit button

The submit handler reads the value with FormData and puts the form into a pending state: a <fieldset disabled> locks the field and the button, and the button shows a spinner with “Saving…”. The spinner is aria-hidden because the button text already says what is happening, and --spinner-color: currentColor keeps it visible on the filled button.

button-pending-state.tsx · 39 lines
import { Button, HelperText, Label, Spinner, TextField, TextFieldInput } from '@vega-ui/react';
import { CSSProperties, FormEvent, useId, useState } from 'react';

export default function ButtonPendingState() {
  const fieldId = useId();
  const [pending, setPending] = useState(false);
  const [message, setMessage] = useState('');

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const workspace = String(data.get('workspace'));

Layout/Overlay

Layout/Overlay: loading content inside a dialog

The dialog opens immediately and loads its details afterwards. The spinner is centered in an area with a reserved height, so the dialog does not resize twice when the content arrives.

dialog-content-loader.tsx · 68 lines
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Spinner,
  Text,
} from '@vega-ui/react';

Error

Error: failed request with retry

A spinner must not keep spinning after the request has failed. The first attempt ends with an error announced via role='alert' and a Retry button; the retry shows the spinner again with an updated label and then succeeds.

retry-row-still-pending.tsx · 57 lines
import { Button, HelperText, Spinner, Text } from '@vega-ui/react';
import { useEffect, useId, useState } from 'react';

type Status = 'syncing' | 'failed' | 'synced';

export default function RetryRowStillPending() {
  const labelId = useId();
  const [status, setStatus] = useState<Status>('syncing');
  const [attempt, setAttempt] = useState(1);

  useEffect(() => {
    if (status !== 'syncing') return;

Edge

Edge: delayed spinner for fast responses

The spinner appears only if the request takes longer than 400 ms. A fast search finishes without any flash of a loader, a slow one still shows progress.

delayed-spinner.tsx · 45 lines
import { Button, Spinner, Text } from '@vega-ui/react';
import { useEffect, useState } from 'react';

export default function DelayedSpinner() {
  const [pending, setPending] = useState(false);
  const [showSpinner, setShowSpinner] = useState(false);
  const [result, setResult] = useState('');

  useEffect(() => {
    if (!pending) return;

    const timer = setTimeout(() => setShowSpinner(true), 400);

Anatomy

Overview

Spinner is a single-part primitive. The root owns animation, size mapping, and variant color.

Required Parts

Spinner

Required. Renders the animated loading indicator.

Optional Parts

There are no exported child parts.

Composition Order

  1. Spinner

Valid Composition Patterns

inline-status-row.tsx · 13 lines
import { Spinner, Text } from '@vega-ui/react';
import { useId } from 'react';

export default function InlineStatusRow() {
  const labelId = useId();

  return (
    <div style={{ display: 'flex', gap: 'var(--spacing-4)', alignItems: 'center' }}>
      <Spinner size={2} aria-labelledby={labelId} />
      <Text id={labelId} size={2}>Syncing 128 contacts from Google</Text>
    </div>
  );

Invalid Composition Patterns

Spinner used without surrounding context

Users may not know what is loading.

Spinner used as permanent decoration

The component should communicate a temporary pending state.