Wraps any content and turns it into its own shimmering skeleton while
loading is true. The placeholder follows the shape of whatever children
you pass — headings, paragraphs, buttons, avatars, cards — so the same markup
serves as both the real content and its skeleton.
Text becomes one bar per line, as wide as that line’s text — wrapped
paragraphs, inline markup (<p>Hello <strong>world</strong></p>) and
bare text beside a nested block included. The bars are drawn as text
decorations, so their ends are square. The skeleton pulses its
opacity instead of sweeping a gradient — a gentle breathe (to 60% over a 2s
cycle). Text placed directly in
Loading, outside any element, is hidden without a bar.
Images and video keep their box and shimmer inside it.
Empty decorative elements (spacers, separators) are left alone.
Bars keep each component’s own colour — a success badge shimmers
green, a primary button blue — while plain text follows the region’s
colorPalette (neutral by default). The tint is translucent, so it reads
on any surface in light and dark mode.
Rendered without children, <Loading /> is a standalone spinner (see
Standalone spinner).
Two loading semantics, picked with the variant prop:
skeleton (default) — you don’t have the content yet. Every descendant
is replaced with shimmer so the user sees the shape of what’s loading.
Reach for this on first render, after a navigation, or when the content
structure is known but the data isn’t.
busy — you already have the content, but an action is in flight
(saving a form, refreshing a feed, running a search, a button’s request).
The content stays exactly as it is — nothing pulses, so nothing swings its
contrast — and an indicator says it’s working. Users keep
their context, they just can’t interact until it resolves.
Rule of thumb: if the user has never seen this content before, use
skeleton. If they’re waiting for the same content to update, use busy.
Loading renders its root with display: contents, so the wrapper disappears
from layout and the children sit directly in their natural flow (the bar and
overlay indicators are the exception: they stack over the content, so the
root becomes a one-cell grid). Wrap at the granularity you want the loading
state to cover:
Wrapping a single card gives you a card-level skeleton.
Wrapping a list wrapper gives you a list-level skeleton — each row already
composes into its own shape.
Wrapping a form lets you show busy across the whole form including its
submit button.
Wrapping a single button with indicator="inline" gives you a loading
button (see Buttons).
For the rare mixed cases, drop data-loading-as on any descendant to change
the treatment locally:
data-loading-as="skeleton" — force skeleton on this subtree even if the
ancestor is busy (e.g. a stat number that’s still being computed while the
rest of the form blurs).
data-loading-as="busy" — keep this subtree as it is inside a skeleton
(e.g. a persistent “Saving…” button that stays readable); still inert.
data-loading-as="none" — opt a subtree out entirely (e.g. a progress bar
or spinner that drives the loading state itself). An opted-out element stays
fully visible and still. Text beside it in the same parent still gets a bar,
down to two levels below the content’s top-level elements; deeper than that,
the neighbouring text is hidden without one.
Loading never needs to know which components it wraps. A component tells it
how to draw itself through the shape contract: its connect bakes
data-loading-shape onto its root.
data-loading-shape
Treatment
block
One shimmer block in the component’s own shape (radius included); its inner text and icons are hidden
circle
A block with a fully rounded radius (avatars, round stamps)
skip
Left out of the shimmer entirely (live regions, decorative indicators)
(absent)
Walked like plain markup: text becomes line bars, media shimmers in place
A new component opts in with one attribute in its own connect — the loading
recipe never lists components. Components that don’t carry the attribute yet
fall back to built-in detection (buttons, badges, stamps, checkboxes, radios,
alerts and wrapped form controls become blocks).
Portaled layers (an open popover or tooltip) render outside the region, so
they are not shimmered. The inert content can’t open new ones.
The content sits in an inert wrapper, so keyboard focus can’t land on a
placeholder and stale or fake content leaves the accessibility tree.
A visually hidden polite status region outside that wrapper announces
label (default "Loading") when loading starts. Pass a translated
label for other languages.
The shimmer animations respect prefers-reduced-motion and collapse to a
static state.
Toggle the loading state — the same markup serves as both the real content and
its skeleton. Every line of text becomes a bar exactly as long as its words,
inline markup and links included, with nothing extra in the markup. Plain text
has no busy treatment — busy keeps text readable and as it is — so this demo
only shows skeleton; the demos below pair it with components.
Loading
Planning the spring trip
The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions.
Send dietary needs to Marta by Friday, and add your talk to the shared agenda so we can plan the schedule.
---// GENERATED from packages/demos/loading/default.demo.tsx — edit the source, not this file.import Loading from "@pindoba/astro-loading";import Checkbox from "@pindoba/astro-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const loading = true;---<div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })} data-ir-root="loading/default"> <Checkbox id="astro-demo-loading-default-toggle" appearance="switch" checked data-ir="1" > Loading </Checkbox> <Loading loading={loading} data-ir="2"> <article> <h3 class={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Planning the spring trip </h3> <p class={css({ mb: "sm" })}> The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions. </p> <p> Send dietary needs to <strong>Marta</strong> by Friday, and add your talk to the <a href="#agenda">shared agenda</a> so we can plan the schedule. </p> </article> </Loading></div><script> import { hydrate } from "./_client/default.ts"; document .querySelectorAll('[data-ir-root="loading/default"]') .forEach((root) => hydrate([root]));</script>
Loading
Planning the spring trip
The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions.
Send dietary needs to Marta by Friday, and add your talk to the shared agenda so we can plan the schedule.
// GENERATED from packages/demos/loading/default.demo.tsx — edit the source, not this file.import { useState } from "react";import { Loading } from "@pindoba/react-loading";import { Checkbox } from "@pindoba/react-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";export default function DefaultDemo() { const [loading, setLoading] = useState(true); return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="react-demo-loading-default-toggle" appearance="switch" checked={loading} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <Loading loading={loading}> <article> <h3 className={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Planning the spring trip </h3> <p className={css({ mb: "sm" })}> The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions. </p> <p> Send dietary needs to <strong>Marta</strong> by Friday, and add your talk to the <a href="#agenda">shared agenda</a> so we can plan the schedule. </p> </article> </Loading> </div> );}
Loading
Planning the spring trip
The team meets in Lisbon for three days in April. Flights are booked for
Tuesday morning, and the venue near the river holds up to forty people
with room for breakout sessions.
Send dietary needs to Marta by Friday, and add your
talk to the shared agenda so we can plan the schedule.
<script lang="ts"> // GENERATED from packages/demos/loading/default.demo.tsx — edit the source, not this file. import Loading from "@pindoba/svelte-loading"; import Checkbox from "@pindoba/svelte-checkbox"; import { stack } from "@pindoba/styled-system/patterns"; import { css } from "@pindoba/styled-system/css"; let loading = $state(true);</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="svelte-demo-loading-default-toggle" appearance="switch" bind:checked={loading} > Loading </Checkbox> <Loading {loading}> <article> <h3 class={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Planning the spring trip </h3> <p class={css({ mb: "sm" })}> The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions. </p> <p> Send dietary needs to <strong> Marta </strong> by Friday, and add your talk to the <a href="#agenda"> shared agenda </a> so we can plan the schedule. </p> </article> </Loading></div>
Loading
Planning the spring trip
The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions.
Send dietary needs to Marta by Friday, and add your talk to the shared agenda so we can plan the schedule.
<script setup lang="ts">// GENERATED from packages/demos/loading/default.demo.tsx — edit the source, not this file.import { ref } from "vue";import { Loading } from "@pindoba/vue-loading";import { Checkbox } from "@pindoba/vue-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const loading = ref(true);</script><template> <div :class="stack({ gap: 'md', direction: 'column', maxWidth: 'lg' })"> <Checkbox id="vue-demo-loading-default-toggle" appearance="switch" :checked="loading" :onChange=" (e: Event) => (loading = (e.target as HTMLInputElement).checked) " > Loading </Checkbox> <Loading :loading="loading"> <article> <h3 :class="css({ fontSize: 'xl', fontWeight: 'bold', mb: 'sm' })"> Planning the spring trip </h3> <p :class="css({ mb: 'sm' })"> The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions. </p> <p> Send dietary needs to <strong>Marta</strong> by Friday, and add your talk to the <a href="#agenda">shared agenda</a> so we can plan the schedule. </p> </article> </Loading> </div></template>
Loading
Planning the spring trip
The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions.
Send dietary needs to Marta by Friday, and add your talk to the shared agenda so we can plan the schedule.
// GENERATED from packages/demos/loading/default.demo.tsx — edit the source, not this file.import { createSignal } from "solid-js";import { Loading } from "@pindoba/solid-loading";import { Checkbox } from "@pindoba/solid-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";export default function DefaultDemo() { const [loading, setLoading] = createSignal(true); return ( <div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="solid-demo-loading-default-toggle" appearance="switch" checked={loading()} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <Loading loading={loading()}> <article> <h3 class={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Planning the spring trip </h3> <p class={css({ mb: "sm" })}> The team meets in Lisbon for three days in April. Flights are booked for Tuesday morning, and the venue near the river holds up to forty people with room for breakout sessions. </p> <p> Send dietary needs to <strong>Marta</strong> by Friday, and add your talk to the <a href="#agenda">shared agenda</a> so we can plan the schedule. </p> </article> </Loading> </div> );}
A profile card composed of Card, Badge and Button. In skeleton
everything inside the card becomes a placeholder without extra markup; switch to
busy and the card stays as it is under the overlay, which says it’s working.
Loading
Analytical Engine
Ada Lovelace · 1843
ResearchPublished
The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform.
---// GENERATED from packages/demos/loading/card.demo.tsx — edit the source, not this file.import Loading from "@pindoba/astro-loading";import Card from "@pindoba/astro-card";import Button from "@pindoba/astro-button";import Badge from "@pindoba/astro-badge";import Checkbox from "@pindoba/astro-checkbox";import SelectNative from "@pindoba/astro-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];const loading = true;const variant = "skeleton" as "skeleton" | "busy";---<div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })} data-ir-root="loading/card"> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })}> <Checkbox id="astro-demo-loading-card-toggle" appearance="switch" checked data-ir="1" > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={"skeleton" as "skeleton" | "busy"} autocomplete="off" data-ir="2" /> </label> </div> <Loading loading={loading} variant={variant} data-ir="3"> <Card header={{ heading: "Analytical Engine", subheading: "Ada Lovelace · 1843", }} > <div class={stack({ gap: "md", direction: "column" })}> <div class={hstack({ gap: "xs", flexWrap: "wrap" })}> <Badge tone="primary">Research</Badge> <Badge tone="success">Published</Badge> </div> <p class={css({ margin: "0" })}> The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. </p> <div class={hstack({ gap: "sm" })}> <Button emphasis="primary">Read more</Button> <Button emphasis="ghost">Share</Button> </div> </div> </Card> </Loading></div><script> import { hydrate } from "./_client/card.ts"; document .querySelectorAll('[data-ir-root="loading/card"]') .forEach((root) => hydrate([root]));</script>
Loading
Analytical Engine
Ada Lovelace · 1843
ResearchPublished
The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform.
// GENERATED from packages/demos/loading/card.demo.tsx — edit the source, not this file.import { useState } from "react";import { Loading } from "@pindoba/react-loading";import { Card } from "@pindoba/react-card";import { Button } from "@pindoba/react-button";import { Badge } from "@pindoba/react-badge";import { Checkbox } from "@pindoba/react-checkbox";import { SelectNative } from "@pindoba/react-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];export default function CardDemo() { const [loading, setLoading] = useState(true); const [variant, setVariant] = useState("skeleton" as "skeleton" | "busy"); return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div className={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap", })} > <Checkbox id="react-demo-loading-card-toggle" appearance="switch" checked={loading} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <label className={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={variant} onChange={(e) => setVariant(e.currentTarget.value as never)} autoComplete="off" /> </label> </div> <Loading loading={loading} variant={variant}> <Card header={{ heading: "Analytical Engine", subheading: "Ada Lovelace · 1843", }} > <div className={stack({ gap: "md", direction: "column" })}> <div className={hstack({ gap: "xs", flexWrap: "wrap" })}> <Badge tone="primary">Research</Badge> <Badge tone="success">Published</Badge> </div> <p className={css({ margin: "0" })}> The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. </p> <div className={hstack({ gap: "sm" })}> <Button emphasis="primary">Read more</Button> <Button emphasis="ghost">Share</Button> </div> </div> </Card> </Loading> </div> );}
Loading
Analytical Engine
Ada Lovelace · 1843
Research Published
The Analytical Engine has no pretensions whatever to originate
anything. It can do whatever we know how to order it to perform.
<script lang="ts"> // GENERATED from packages/demos/loading/card.demo.tsx — edit the source, not this file. import Loading from "@pindoba/svelte-loading"; import Card from "@pindoba/svelte-card"; import Button from "@pindoba/svelte-button"; import Badge from "@pindoba/svelte-badge"; import Checkbox from "@pindoba/svelte-checkbox"; import SelectNative from "@pindoba/svelte-select-native"; import { stack, hstack } from "@pindoba/styled-system/patterns"; import { css } from "@pindoba/styled-system/css"; const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" }, ]; let loading = $state(true); let variant = $state("skeleton" as "skeleton" | "busy");</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })}> <Checkbox id="svelte-demo-loading-card-toggle" appearance="switch" bind:checked={loading} > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span> Variant </span> <SelectNative items={variants} bind:value={variant} autocomplete="off" ></SelectNative> </label> </div> <Loading {loading} {variant}> <Card header={{ heading: "Analytical Engine", subheading: "Ada Lovelace · 1843", }} > <div class={stack({ gap: "md", direction: "column" })}> <div class={hstack({ gap: "xs", flexWrap: "wrap" })}> <Badge tone="primary">Research</Badge> <Badge tone="success">Published</Badge> </div> <p class={css({ margin: "0" })}> The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. </p> <div class={hstack({ gap: "sm" })}> <Button emphasis="primary">Read more</Button> <Button emphasis="ghost">Share</Button> </div> </div> </Card> </Loading></div>
Loading
Analytical Engine
Ada Lovelace · 1843
ResearchPublished
The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform.
<script setup lang="ts">// GENERATED from packages/demos/loading/card.demo.tsx — edit the source, not this file.import { ref } from "vue";import { Loading } from "@pindoba/vue-loading";import { Card } from "@pindoba/vue-card";import { Button } from "@pindoba/vue-button";import { Badge } from "@pindoba/vue-badge";import { Checkbox } from "@pindoba/vue-checkbox";import { SelectNative } from "@pindoba/vue-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];const loading = ref(true);const variant = ref("skeleton" as "skeleton" | "busy");</script><template> <div :class="stack({ gap: 'md', direction: 'column', maxWidth: 'lg' })"> <div :class="hstack({ gap: 'md', alignItems: 'center', flexWrap: 'wrap' })"> <Checkbox id="vue-demo-loading-card-toggle" appearance="switch" :checked="loading" :onChange=" (e: Event) => (loading = (e.target as HTMLInputElement).checked) " > Loading </Checkbox> <label :class="hstack({ gap: 'xs', alignItems: 'center' })"> <span>Variant</span> <SelectNative :items="variants" :value="variant" :onChange=" (e: Event) => (variant = (e.target as HTMLSelectElement).value as never) " autocomplete="off" ></SelectNative> </label> </div> <Loading :loading="loading" :variant="variant"> <Card :header="{ heading: 'Analytical Engine', subheading: 'Ada Lovelace · 1843', }" > <div :class="stack({ gap: 'md', direction: 'column' })"> <div :class="hstack({ gap: 'xs', flexWrap: 'wrap' })"> <Badge tone="primary">Research</Badge> <Badge tone="success">Published</Badge> </div> <p :class="css({ margin: '0' })"> The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. </p> <div :class="hstack({ gap: 'sm' })"> <Button emphasis="primary">Read more</Button> <Button emphasis="ghost">Share</Button> </div> </div> </Card> </Loading> </div></template>
Loading
Analytical Engine
Ada Lovelace · 1843
ResearchPublished
The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform.
// GENERATED from packages/demos/loading/card.demo.tsx — edit the source, not this file.import { createSignal } from "solid-js";import { Loading } from "@pindoba/solid-loading";import { Card } from "@pindoba/solid-card";import { Button } from "@pindoba/solid-button";import { Badge } from "@pindoba/solid-badge";import { Checkbox } from "@pindoba/solid-checkbox";import { SelectNative } from "@pindoba/solid-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];export default function CardDemo() { const [loading, setLoading] = createSignal(true); const [variant, setVariant] = createSignal("skeleton" as "skeleton" | "busy"); return ( <div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })} > <Checkbox id="solid-demo-loading-card-toggle" appearance="switch" checked={loading()} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={variant()} onChange={(e) => setVariant(e.currentTarget.value as never)} autocomplete="off" /> </label> </div> <Loading loading={loading()} variant={variant()}> <Card header={{ heading: "Analytical Engine", subheading: "Ada Lovelace · 1843", }} > <div class={stack({ gap: "md", direction: "column" })}> <div class={hstack({ gap: "xs", flexWrap: "wrap" })}> <Badge tone="primary">Research</Badge> <Badge tone="success">Published</Badge> </div> <p class={css({ margin: "0" })}> The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. </p> <div class={hstack({ gap: "sm" })}> <Button emphasis="primary">Read more</Button> <Button emphasis="ghost">Share</Button> </div> </div> </Card> </Loading> </div> );}
Loading over messaging components like Banner and Alert. In skeleton
each alert collapses into one block and its icon is hidden; in busy the alerts
stay exactly as they are.
Loading
Workspace usage
You have used 62% of your monthly quota.
Your profile changes were saved successfully and published to the team.
Billing cycle ends in 3 days. Review your plan before renewal.
---// GENERATED from packages/demos/loading/banner.demo.tsx — edit the source, not this file.import Loading from "@pindoba/astro-loading";import Banner from "@pindoba/astro-banner";import Alert from "@pindoba/astro-alert";import Checkbox from "@pindoba/astro-checkbox";import SelectNative from "@pindoba/astro-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];const loading = true;const variant = "skeleton" as "skeleton" | "busy";---<div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })} data-ir-root="loading/banner"> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })}> <Checkbox id="astro-demo-loading-banner-toggle" appearance="switch" checked data-ir="1" > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={"skeleton" as "skeleton" | "busy"} autocomplete="off" data-ir="2" /> </label> </div> <Loading loading={loading} variant={variant} data-ir="3"> <div class={stack({ gap: "md", direction: "column" })}> <Banner heading="Workspace usage" subheading="You have used 62% of your monthly quota." /> <Alert tone="success"> Your profile changes were saved successfully and published to the team. </Alert> <Alert tone="warning"> Billing cycle ends in 3 days. Review your plan before renewal. </Alert> </div> </Loading></div><script> import { hydrate } from "./_client/banner.ts"; document .querySelectorAll('[data-ir-root="loading/banner"]') .forEach((root) => hydrate([root]));</script>
Loading
Workspace usage
You have used 62% of your monthly quota.
Your profile changes were saved successfully and published to the team.
Billing cycle ends in 3 days. Review your plan before renewal.
// GENERATED from packages/demos/loading/banner.demo.tsx — edit the source, not this file.import { useState } from "react";import { Loading } from "@pindoba/react-loading";import { Banner } from "@pindoba/react-banner";import { Alert } from "@pindoba/react-alert";import { Checkbox } from "@pindoba/react-checkbox";import { SelectNative } from "@pindoba/react-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];export default function BannerDemo() { const [loading, setLoading] = useState(true); const [variant, setVariant] = useState("skeleton" as "skeleton" | "busy"); return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div className={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap", })} > <Checkbox id="react-demo-loading-banner-toggle" appearance="switch" checked={loading} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <label className={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={variant} onChange={(e) => setVariant(e.currentTarget.value as never)} autoComplete="off" /> </label> </div> <Loading loading={loading} variant={variant}> <div className={stack({ gap: "md", direction: "column" })}> <Banner heading="Workspace usage" subheading="You have used 62% of your monthly quota." /> <Alert tone="success"> Your profile changes were saved successfully and published to the team. </Alert> <Alert tone="warning"> Billing cycle ends in 3 days. Review your plan before renewal. </Alert> </div> </Loading> </div> );}
Loading
Workspace usage
You have used 62% of your monthly quota.
Your profile changes were saved successfully and published to the team.
Billing cycle ends in 3 days. Review your plan before renewal.
<script lang="ts"> // GENERATED from packages/demos/loading/banner.demo.tsx — edit the source, not this file. import Loading from "@pindoba/svelte-loading"; import Banner from "@pindoba/svelte-banner"; import Alert from "@pindoba/svelte-alert"; import Checkbox from "@pindoba/svelte-checkbox"; import SelectNative from "@pindoba/svelte-select-native"; import { stack, hstack } from "@pindoba/styled-system/patterns"; const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" }, ]; let loading = $state(true); let variant = $state("skeleton" as "skeleton" | "busy");</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })}> <Checkbox id="svelte-demo-loading-banner-toggle" appearance="switch" bind:checked={loading} > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span> Variant </span> <SelectNative items={variants} bind:value={variant} autocomplete="off" ></SelectNative> </label> </div> <Loading {loading} {variant}> <div class={stack({ gap: "md", direction: "column" })}> <Banner heading="Workspace usage" subheading="You have used 62% of your monthly quota." ></Banner> <Alert tone="success"> Your profile changes were saved successfully and published to the team. </Alert> <Alert tone="warning"> Billing cycle ends in 3 days. Review your plan before renewal. </Alert> </div> </Loading></div>
Loading
Workspace usage
You have used 62% of your monthly quota.
Your profile changes were saved successfully and published to the team.
Billing cycle ends in 3 days. Review your plan before renewal.
<script setup lang="ts">// GENERATED from packages/demos/loading/banner.demo.tsx — edit the source, not this file.import { ref } from "vue";import { Loading } from "@pindoba/vue-loading";import { Banner } from "@pindoba/vue-banner";import { Alert } from "@pindoba/vue-alert";import { Checkbox } from "@pindoba/vue-checkbox";import { SelectNative } from "@pindoba/vue-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];const loading = ref(true);const variant = ref("skeleton" as "skeleton" | "busy");</script><template> <div :class="stack({ gap: 'md', direction: 'column', maxWidth: 'lg' })"> <div :class="hstack({ gap: 'md', alignItems: 'center', flexWrap: 'wrap' })"> <Checkbox id="vue-demo-loading-banner-toggle" appearance="switch" :checked="loading" :onChange=" (e: Event) => (loading = (e.target as HTMLInputElement).checked) " > Loading </Checkbox> <label :class="hstack({ gap: 'xs', alignItems: 'center' })"> <span>Variant</span> <SelectNative :items="variants" :value="variant" :onChange=" (e: Event) => (variant = (e.target as HTMLSelectElement).value as never) " autocomplete="off" ></SelectNative> </label> </div> <Loading :loading="loading" :variant="variant"> <div :class="stack({ gap: 'md', direction: 'column' })"> <Banner heading="Workspace usage" subheading="You have used 62% of your monthly quota." ></Banner> <Alert tone="success" >Your profile changes were saved successfully and published to the team.</Alert > <Alert tone="warning" >Billing cycle ends in 3 days. Review your plan before renewal.</Alert > </div> </Loading> </div></template>
Loading
Workspace usage
You have used 62% of your monthly quota.
Your profile changes were saved successfully and published to the team.
Billing cycle ends in 3 days. Review your plan before renewal.
// GENERATED from packages/demos/loading/banner.demo.tsx — edit the source, not this file.import { createSignal } from "solid-js";import { Loading } from "@pindoba/solid-loading";import { Banner } from "@pindoba/solid-banner";import { Alert } from "@pindoba/solid-alert";import { Checkbox } from "@pindoba/solid-checkbox";import { SelectNative } from "@pindoba/solid-select-native";import { stack, hstack } from "@pindoba/styled-system/patterns";const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];export default function BannerDemo() { const [loading, setLoading] = createSignal(true); const [variant, setVariant] = createSignal("skeleton" as "skeleton" | "busy"); return ( <div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })} > <Checkbox id="solid-demo-loading-banner-toggle" appearance="switch" checked={loading()} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <SelectNative items={variants} value={variant()} onChange={(e) => setVariant(e.currentTarget.value as never)} autocomplete="off" /> </label> </div> <Loading loading={loading()} variant={variant()}> <div class={stack({ gap: "md", direction: "column" })}> <Banner heading="Workspace usage" subheading="You have used 62% of your monthly quota." /> <Alert tone="success"> Your profile changes were saved successfully and published to the team. </Alert> <Alert tone="warning"> Billing cycle ends in 3 days. Review your plan before renewal. </Alert> </div> </Loading> </div> );}
A form while it saves — the case busy is for. The fields, select, checkbox
and buttons stay readable and inert under the overlay; switch to
skeleton for a form whose data is still loading.
Mark a subtree with data-loading-as="none" to keep it out of the treatment —
here the Progress bar and the Cancel Button that drive the sync stay live
and clickable while the repository list loads.
The main place a loading state shows up. Wrap the button with
indicator="inline": it keeps its exact width and its fill — it’s working,
not disabled — and its label is swapped for a spinner drawn inside it, so the
spinner takes the button’s own text colour (white on primary, ink on
ghost). The label still names the button for assistive technology, and the
label prop is announced while it’s busy.
For a card, a panel or a whole form: a light tint over the content and a card
with the spinner, the label as a visible message and — with progress — the
percentage. Set --pindoba-loading-radius on the region to match the content’s
corner radius (default md).
Over a region taller than the screen, the card stays on screen: it keeps to the
middle of the viewport while the region fills it, and to the screen’s edge
while only part of the region shows. Inside a scrolling container, that
container is the screen.
Saving your profile…
---// GENERATED from packages/demos/loading/overlay.demo.tsx — edit the source, not this file.import Loading from "@pindoba/astro-loading";import Input from "@pindoba/astro-input";import Button from "@pindoba/astro-button";import Checkbox from "@pindoba/astro-checkbox";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const saving = false;const progress = undefined as number | undefined;---<div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })} data-ir-root="loading/overlay"> <Checkbox id="astro-demo-loading-overlay-progress" appearance="switch" data-ir="1" > Report progress </Checkbox> <Loading loading={saving} variant="busy" indicator="overlay" label="Saving your profile…" progress={progress} data-ir="2" > <form class={stack({ gap: "md", direction: "column", p: "lg", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} data-ir="3" > <h3 class={css({ fontSize: "lg", fontWeight: "bold", margin: "0" })}> Profile </h3> <Input placeholder="Display name" value="Margaret Hamilton" /> <Input placeholder="Email" type="email" value="margaret@apollo.dev" /> <div class={hstack({ gap: "sm" })}> <Button emphasis="primary" type="submit"> Save profile </Button> <Button emphasis="ghost" type="button"> Cancel </Button> </div> </form> </Loading></div><script> import { hydrate } from "./_client/overlay.ts"; document .querySelectorAll('[data-ir-root="loading/overlay"]') .forEach((root) => hydrate([root]));</script>
Saving your profile…
// GENERATED from packages/demos/loading/overlay.demo.tsx — edit the source, not this file.import { type SyntheticEvent as ReactSyntheticEvent, useState } from "react";import { Loading } from "@pindoba/react-loading";import { Input } from "@pindoba/react-input";import { Button } from "@pindoba/react-button";import { Checkbox } from "@pindoba/react-checkbox";import { stack, hstack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";export default function OverlayDemo() { const [saving, setSaving] = useState(false); const [withProgress, setWithProgress] = useState(false); const [progress, setProgress] = useState(undefined as number | undefined); function step(value: number) { setTimeout( () => { if (value < 100) { step(Math.min(100, value + 4)); return; } setSaving(false); setProgress(undefined); }, value < 100 ? 120 : 400, ); setProgress(value); } function save(event: ReactSyntheticEvent) { event.preventDefault(); if (withProgress) step(0); else setTimeout(() => { setSaving(false); }, 2500); setSaving(true); } return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="react-demo-loading-overlay-progress" appearance="switch" checked={withProgress} onChange={(e) => setWithProgress(e.currentTarget.checked)} > Report progress </Checkbox> <Loading loading={saving} variant="busy" indicator="overlay" label="Saving your profile…" progress={progress} > <form className={stack({ gap: "md", direction: "column", p: "lg", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} onSubmit={save} > <h3 className={css({ fontSize: "lg", fontWeight: "bold", margin: "0" })} > Profile </h3> <Input placeholder="Display name" defaultValue="Margaret Hamilton" /> <Input placeholder="Email" type="email" defaultValue="margaret@apollo.dev" /> <div className={hstack({ gap: "sm" })}> <Button emphasis="primary" type="submit"> Save profile </Button> <Button emphasis="ghost" type="button"> Cancel </Button> </div> </form> </Loading> </div> );}
Saving your profile…
<script lang="ts"> // GENERATED from packages/demos/loading/overlay.demo.tsx — edit the source, not this file. import Loading from "@pindoba/svelte-loading"; import Input from "@pindoba/svelte-input"; import Button from "@pindoba/svelte-button"; import Checkbox from "@pindoba/svelte-checkbox"; import { stack, hstack } from "@pindoba/styled-system/patterns"; import { css } from "@pindoba/styled-system/css"; let saving = $state(false); let withProgress = $state(false); let progress = $state(undefined as number | undefined); function step(value: number) { setTimeout( () => { if (value < 100) { step(Math.min(100, value + 4)); return; } saving = false; progress = undefined; }, value < 100 ? 120 : 400, ); progress = value; } function save(event: SubmitEvent) { event.preventDefault(); if (withProgress) step(0); else setTimeout(() => { saving = false; }, 2500); saving = true; }</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="svelte-demo-loading-overlay-progress" appearance="switch" bind:checked={withProgress} > Report progress </Checkbox> <Loading loading={saving} variant="busy" indicator="overlay" label="Saving your profile…" {progress} > <form class={stack({ gap: "md", direction: "column", p: "lg", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} onsubmit={save} > <h3 class={css({ fontSize: "lg", fontWeight: "bold", margin: "0" })}> Profile </h3> <Input placeholder="Display name" value="Margaret Hamilton"></Input> <Input placeholder="Email" type="email" value="margaret@apollo.dev" ></Input> <div class={hstack({ gap: "sm" })}> <Button emphasis="primary" type="submit">Save profile</Button> <Button emphasis="ghost" type="button">Cancel</Button> </div> </form> </Loading></div>
For small blocks and rows, where a card over the content would cover it: a
spinner stamp on the top-end corner, rendered via Attachment.
Loading
Saving changes…
A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.
---// GENERATED from packages/demos/loading/indicator.demo.tsx — edit the source, not this file.import Loading from "@pindoba/astro-loading";import Checkbox from "@pindoba/astro-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const loading = true;---<div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })} data-ir-root="loading/indicator"> <Checkbox id="astro-demo-loading-indicator-toggle" appearance="switch" checked data-ir="1" > Loading </Checkbox> <Loading loading={loading} variant="busy" indicator data-ir="2"> <article class={css({ p: "md", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} > <h3 class={css({ fontSize: "lg", fontWeight: "bold", mb: "xs" })}> Saving changes… </h3> <p> <span> A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable. </span> </p> </article> </Loading></div><script> import { hydrate } from "./_client/indicator.ts"; document .querySelectorAll('[data-ir-root="loading/indicator"]') .forEach((root) => hydrate([root]));</script>
Saving changes…
A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.
// GENERATED from packages/demos/loading/indicator.demo.tsx — edit the source, not this file.import { useState } from "react";import { Loading } from "@pindoba/react-loading";import { Checkbox } from "@pindoba/react-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";export default function IndicatorDemo() { const [loading, setLoading] = useState(true); return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="react-demo-loading-indicator-toggle" appearance="switch" checked={loading} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <Loading loading={loading} variant="busy" indicator> <article className={css({ p: "md", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} > <h3 className={css({ fontSize: "lg", fontWeight: "bold", mb: "xs" })}> Saving changes… </h3> <p> <span> A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable. </span> </p> </article> </Loading> </div> );}
Saving changes…
A short status region where the shimmer alone could be easy to miss.
The corner spinner makes the busy state unmistakable.
<script lang="ts"> // GENERATED from packages/demos/loading/indicator.demo.tsx — edit the source, not this file. import Loading from "@pindoba/svelte-loading"; import Checkbox from "@pindoba/svelte-checkbox"; import { stack } from "@pindoba/styled-system/patterns"; import { css } from "@pindoba/styled-system/css"; let loading = $state(true);</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="svelte-demo-loading-indicator-toggle" appearance="switch" bind:checked={loading} > Loading </Checkbox> <Loading {loading} variant="busy" indicator> <article class={css({ p: "md", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} > <h3 class={css({ fontSize: "lg", fontWeight: "bold", mb: "xs" })}> Saving changes… </h3> <p> <span> A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable. </span> </p> </article> </Loading></div>
Saving changes…
A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.
<script setup lang="ts">// GENERATED from packages/demos/loading/indicator.demo.tsx — edit the source, not this file.import { ref } from "vue";import { Loading } from "@pindoba/vue-loading";import { Checkbox } from "@pindoba/vue-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";const loading = ref(true);</script><template> <div :class="stack({ gap: 'md', direction: 'column', maxWidth: 'lg' })"> <Checkbox id="vue-demo-loading-indicator-toggle" appearance="switch" :checked="loading" :onChange=" (e: Event) => (loading = (e.target as HTMLInputElement).checked) " > Loading </Checkbox> <Loading :loading="loading" variant="busy" :indicator="true"> <article :class=" css({ p: 'md', borderWidth: '1px', borderStyle: 'solid', borderColor: 'neutral.border.muted', borderRadius: 'md', }) " > <h3 :class="css({ fontSize: 'lg', fontWeight: 'bold', mb: 'xs' })"> Saving changes… </h3> <p> <span >A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.</span > </p> </article> </Loading> </div></template>
Saving changes…
A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.
// GENERATED from packages/demos/loading/indicator.demo.tsx — edit the source, not this file.import { createSignal } from "solid-js";import { Loading } from "@pindoba/solid-loading";import { Checkbox } from "@pindoba/solid-checkbox";import { stack } from "@pindoba/styled-system/patterns";import { css } from "@pindoba/styled-system/css";export default function IndicatorDemo() { const [loading, setLoading] = createSignal(true); return ( <div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <Checkbox id="solid-demo-loading-indicator-toggle" appearance="switch" checked={loading()} onChange={(e) => setLoading(e.currentTarget.checked)} > Loading </Checkbox> <Loading loading={loading()} variant="busy" indicator> <article class={css({ p: "md", borderWidth: "1px", borderStyle: "solid", borderColor: "neutral.border.muted", borderRadius: "md", })} > <h3 class={css({ fontSize: "lg", fontWeight: "bold", mb: "xs" })}> Saving changes… </h3> <p> <span> A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable. </span> </p> </article> </Loading> </div> );}
When you know how far along the work is (uploads, imports), pass progress
(0–100). The indicator’s spinner becomes a ring — or, for bar, a line — that
fills to it; inline and overlay show the percentage too. Leave it unset
while the progress is unknown. Screen readers hear the label with the
progress only as it passes each quarter (0, 25, 50, 75, 100%), never on every
update.
A loading state that flashes for a fraction of a second is worse than none, and
one that blinks off and on again is worse still. So busy waits 300ms
(delay) before showing anything — most saves finish sooner and never show it —
and, once shown, stays at least 500ms (minDuration). The content turns
inert the moment loading is set, so a second click can’t land while the
delay runs. skeleton shows at once (delay and minDuration default to 0):
it stands in for content that isn’t there.
The skeleton’s pulse cycle is set with speed: slow (3s), normal (2s, the
default) or fast (1.5s). It dims to 60%; set --pindoba-loading-floor on the
region (e.g. 0.8) for a shallower pulse. With reduced motion, nothing pulses
or spins.
Without children, <Loading /> renders a self-contained inline spinner with
the same polite status announcement — the generic “something is loading”
element for a list’s loading row, a toolbar or a status line. Its presence is
the loading state, so loading isn’t needed. Size it with size
(xs–lg, default sm) and name it with label.
In Svelte, React, Vue and Solid a loading region follows its props — flip
loading or variant in state and it re-renders. Astro renders once on the
server, so it gets a runtime path instead: render the region with the
reactive prop (which serializes its config into the HTML) and call
updateLoadingElement() from @pindoba/astro-loading/runtime:
The patch re-runs the connect in the browser and updates the region’s classes,
data-loading, aria-busy, the content’s inert and the status text in
place; loading, variant, speed and progress are patchable, and a
loading change honours the region’s delay / minDuration. A reactive
region always renders its indicator and toggles its hidden state, so it can
appear later without new markup. Every Astro demo on this page drives its switch and select this
way. The same function works from vanilla JavaScript via
@pindoba/core-loading.