A turning circle that says something is running, without saying how far (also called a loading indicator). Reach for it for short waits whose length nobody knows, beside a word that says what runs.
import { Spinner } from "@umriss-ui/core";
It takes the colour of the text around it and announces itself as “Loading”; name it better with aria-label where the word beside it is not enough.
A spinner says only that something runs; the words beside it say what.
import { Spinner, Stack, Text } from "@umriss-ui/core";
export default function WithAWord() {
return (
<Stack direction="row" gap={2} align="center">
<Spinner aria-label="Loading the tours" />
<Text size="sm" tone="secondary">
Loading the tours of North depot …
</Text>
</Stack>
);
}
size is the edge length in pixels; match it to the text it stands beside – 12 for small print, 14 for body text.
import { Spinner, Stack } from "@umriss-ui/core";
export default function Sizes() {
return (
<Stack direction="row" gap={4} align="center">
<Spinner size={12} />
<Spinner size={14} />
<Spinner size={20} />
<Spinner size={32} />
</Stack>
);
}
It draws in currentColor, so it takes the colour of whatever it stands in and needs no tone of its own.
import { Spinner, Stack, Text } from "@umriss-ui/core";
export default function Colour() {
return (
<Stack direction="row" gap={5} align="center" wrap>
<Stack direction="row" gap={2} align="center">
<Spinner size={14} />
<Text size="sm">Refreshing</Text>
</Stack>
<Stack direction="row" gap={2} align="center" style={{ color: "var(--u-color-accent)" }}>
<Spinner size={14} />
<Text as="span" size="sm" style={{ color: "inherit" }}>
Syncing the calendar
</Text>
</Stack>
</Stack>
);
}
While a panel reloads, the button that started it carries the spinner - loading on a Button - and the old figures stay readable below it. The button stays, so the head keeps its height.
import { useEffect, useState } from "react";
import { Button, Card, CardBody, CardHeader, Stack, Text } from "@umriss-ui/core";
export default function APanelRefreshing() {
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!loading) return;
const timer = setTimeout(() => setLoading(false), 1500);
return () => clearTimeout(timer);
}, [loading]);
return (
<Card style={{ maxWidth: 420 }}>
<CardHeader
title="Open incidents"
actions={
<Button size="sm" variant="ghost" loading={loading} onClick={() => setLoading(true)}>
Refresh
</Button>
}
/>
<CardBody>
<Stack gap={2}>
<Text size="sm">INC-1048 · Checkout slow, card payments time out</Text>
<Text size="sm">INC-1047 · Webhook deliveries delayed</Text>
</Stack>
</CardBody>
</Card>
);
}
loading on ButtonThe keys of Button apply here.
An svg with the role status, named "Loading" by the wording or by your aria-label; the role stays whatever you pass. It draws no text, so its name is all a screen reader hears of it. Where a word beside it already says what runs, as in a loading toast, hide it with aria-hidden.
With reduced motion it turns at less than half its speed instead of stopping, so it still says that something runs.
SpinnerProps| Name | Type | Default | Description |
|---|---|---|---|
size | number | 14 | Edge length in pixels. |
ref | Ref<SVGSVGElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
Also takes every attribute of <svg>.
What umriss deliberately does not build, and why: ADR-0032.