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
- Oklab and OkLCh — comes from the design tokens
- :where()
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
SpinnerPropsSpinnerSizeSpinnerVariant
Basic Usage
import { Spinner } from '@vega-ui/react';
export default function BasicUsage() {
return <Spinner size={3} aria-label='Loading messages' />;
}Variants
- Size:
1through11 - Variant:
primaryorsecondary - 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
Progressbar 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.
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.
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.
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.
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.
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.
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
Spinner
Valid Composition Patterns
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.