UI componentsdocs/ui/components/progress.md

Progress & loading

Show that work is in progress, or load more.

← Component reference

Spinner #

Loading indicator.

<Spinner label="Loading deploys" />
PropTypeRequiredDefaultDescription
labelstring"Loading"Accessible name announced to screen readers.
size"sm" | "md" | "lg""md"Spinner size.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Progress #

Bars, steps, rings and semicircles.

Progress #

Linear progress bar.

<Progress value={64} color="success" />

Slots: root bar

PropTypeRequiredDefaultDescription
valuenumberyesCurrent progress, from 0 to max.
color"warning" | "success" | "info" | "danger" | "primary""primary"Bar colour.
indeterminatebooleanUnknown duration: an animated bar instead of value.
maxnumber100Value at 100%.
size"sm" | "md" | "lg""md"Bar thickness.

ProgressColor

type ProgressColor = "primary" | "info" | "success" | "warning" | "danger" | undefined;

ProgressSize

type ProgressSize = "sm" | "md" | "lg" | undefined;

SteppedProgress #

Progress split into discrete steps.

<SteppedProgress label="Onboarding" steps={4} value={2} />

Slots: root segment

PropTypeRequiredDefaultDescription
stepsnumberyesNumber of segments.
valuenumberyesCompleted segments.
labelstring"Progress"Accessible name (default "Progress").

RingProgress #

Circular progress.

72%
<RingProgress value={72} label="72%" />

Slots: root bar label track

PropTypeRequiredDefaultDescription
valuenumberyesProgress in percent (0–100).
labelcontentContent in the middle, e.g. 72%.
sizenumber72Diameter in pixels.
thicknessnumber6Ring stroke width in pixels.

SemiCircleProgress #

Half-ring gauge.

64%
<SemiCircleProgress value={64} label="64%" />

Slots: root bar label track

PropTypeRequiredDefaultDescription
valuenumberyesProgress in percent (0–100).
labelcontentContent in the middle, e.g. 64%.
sizenumber120Width in pixels.
thicknessnumber10Arc stroke width in pixels.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Top loading bar (Mantine NavigationProgress / NProgress).

40%

function Example() {
    const progress = signal(40);
    const running = signal(true);
    // Trickle towards 90% while "navigating" (effects run only in the browser).
    effect(() => {
        if (!running()) return;
        const timer = setInterval(
            () => progress.set(Math.min(90, progress() + (90 - progress()) * 0.2)),
            300,
        );
        return () => clearInterval(timer);
    });
    return (
        <Stack gap="0.75rem" style={{ "padding-top": "0.75rem" }}>
            {/* Pinned to the top of the page (here: of the preview). */}
            <NavigationProgress visible={progress() < 100} value={progress()} />
            <Group gap="0.5rem">
                <Button
                    size="sm"
                    onClick={() => {
                        progress.set(5);
                        running.set(true);
                    }}
                >
                    Start navigation
                </Button>
                <Button
                    size="sm"
                    variant="outline"
                    onClick={() => {
                        running.set(false);
                        progress.set(100);
                    }}
                >
                    Finish
                </Button>
                <Text muted>{progress() >= 100 ? "Done" : `${Math.round(progress())}%`}</Text>
            </Group>
        </Stack>
    );
}

Slots: root bar

PropTypeRequiredDefaultDescription
visiblebooleanyesShow the bar (hide it when navigation finishes).
labelstring"Loading"Accessible name (default "Loading").
valuenumber300–100; omit for indeterminate.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

LoadingOverlay #

Covers its positioned parent with a spinner while visible.

Deploy list

function Example() {
    const loading = signal(true);
    const refresh = () => loading.set(true);
    // Pretend each refresh takes 1.5 s (effects run only in the browser).
    effect(() => {
        if (!loading()) return;
        const timer = setTimeout(() => loading.set(false), 1500);
        return () => clearTimeout(timer);
    });
    return (
        <Stack gap="0.5rem">
            <Box style={{ position: "relative", "min-height": "6rem" }}>
                <Text>Deploy list</Text>
                <LoadingOverlay visible={loading()} label="Refreshing" />
            </Box>
            <Button size="sm" variant="outline" onClick={refresh} disabled={loading()}>
                Refresh
            </Button>
        </Stack>
    );
}

Slots: root spinner

PropTypeRequiredDefaultDescription
visiblebooleanyesShow the overlay and spinner.
labelstring"Loading"Accessible name of the spinner.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Skeleton #

Placeholders while content loads.

Skeleton #

Loading placeholder sized by width / height.

<Skeleton width="16rem" height="1.25rem" />
PropTypeRequiredDefaultDescription
heightstring"0.9rem"Height (any CSS length).
widthstring"100%"Width (any CSS length).

SkeletonText #

Placeholder text lines.

<SkeletonText lines={3} />

Slots: root line

PropTypeRequiredDefaultDescription
linesnumber3Number of placeholder lines.

SkeletonCard #

Placeholder card.

<SkeletonCard />

Slots: root block text

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Load more #

Incremental loading of lists.

LoadMore #

"Load more" button that shows loading and end-of-list states.

Activity #1

Activity #2

Activity #3

function Example() {
    const count = signal(3);
    const loading = signal(false);
    const load = () => {
        loading.set(true);
        setTimeout(() => {
            count.set(count() + 3);
            loading.set(false);
        }, 500);
    };
    return (
        <Stack gap="0.5rem">
            <For each={Array.from({ length: count() }, (_, i) => i + 1)}>
                {(n) => <Text>Activity #{n}</Text>}
            </For>
            <LoadMore
                onLoad={load}
                loading={loading()}
                hasMore={count() < 12}
                endLabel="That's everything."
            >
                Load 3 more
            </LoadMore>
        </Stack>
    );
}

Slots: root button end

PropTypeRequiredDefaultDescription
onLoad() => voidyesCalled when the user asks for more.
childrencontent"You're all caught up"Button label.
endLabelcontentText when everything is loaded (default "You're all caught up").
hasMorebooleanWhether more items can load; shows endLabel when false.
loadingbooleanShow a spinner and disable the button.

InfiniteScroll #

Content followed by a "Load more" control.

Deploy #128

Deploy #127

Deploy #126

Deploy #125

Deploy #124

Deploy #123

function Example() {
    const deploys = signal(Array.from({ length: 6 }, (_, i) => 128 - i));
    const loading = signal(false);
    const hasMore = () => deploys().length < 30;
    const loadMore = () => {
        if (loading() || !hasMore()) return;
        loading.set(true);
        setTimeout(() => {
            const last = deploys()[deploys().length - 1] ?? 128;
            deploys.set([...deploys(), ...Array.from({ length: 6 }, (_, i) => last - 1 - i)]);
            loading.set(false);
        }, 600);
    };
    return (
        <ScrollArea maxHeight="12rem">
            <InfiniteScroll onLoadMore={loadMore} hasMore={hasMore()} loading={loading()}>
                <Stack gap="0.5rem">
                    <For each={deploys()}>{(n) => <Text>Deploy #{n}</Text>}</For>
                </Stack>
            </InfiniteScroll>
        </ScrollArea>
    );
}

Slots: root sentinel

PropTypeRequiredDefaultDescription
onLoadMore() => voidyesCalled when the end of the list scrolls into view (or the button is pressed).
childrencontentThe list rendered so far.
hasMorebooleanWhether more items can load.
loadingbooleanCurrently loading (prevents duplicate requests, shows a spinner).
loadMoreLabelcontent"Load more"Button text (default "Load more" / "Loading…").
rootMarginstring"200px"How far before the end to start loading (IntersectionObserver rootMargin). Default 200px.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.