Wraps any content and turns it into its own shimmering skeleton while
loading is true. The component applies a transparent text + animated
gradient background to every descendant, so the placeholder naturally follows
the shape of whatever children you pass — headings, paragraphs, buttons,
avatars, cards.
The technique uses color: transparent, a horizontally animated
linear-gradient and -webkit-box-decoration-break: clone so multi-line text
shimmers per line.
While loading, the root element receives aria-busy="true", aria-live="polite"
and inert, so keyboard focus cannot land inside the skeleton.
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). Children stay
legible and simply blur with a gentle opacity pulse. 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. 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.
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" — force busy on this subtree inside a skeleton
(e.g. a persistent “Saving…” button that stays readable).
data-loading-as="none" — opt a subtree out entirely (e.g. a progress bar
or spinner that drives the loading state itself).
The recipe recognises Pindoba components via their data-component attribute
and gives each the right treatment:
Badge, Stamp, Checkbox, Radio — shimmered as atomic blocks so their
pill / circle / square shape is preserved; inner text and icons are hidden.
Button — in skeleton the whole button becomes a shimmer block (no
label); in busy the button keeps its label but blurs and pulses.
Input, Select, Textarea — the padded wrapper shimmers edge-to-edge and
the inner control + placeholder are hidden, so no stray placeholder text
leaks through.
Card, Alert, Banner, and regular text nodes — follow the generic leaf
shimmer, so their internal spans and paragraphs animate line-by-line.
The root receives aria-busy="true", aria-live="polite" and inert while
loading, so assistive tech is notified and keyboard focus can’t land inside a
placeholder. The shimmer and pulse 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. Wrapping text in a <span> lets each wrapped line shimmer
individually.
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non
proident.
---import Default from "@pindoba/svelte-loading/demos/default";---<Default client:load />
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident.
import { useState } from "react";import { Loading } from "../Loading";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 DefaultDemo() { const [loading, setLoading] = useState(true); const [variant, setVariant] = useState<"skeleton" | "busy">("skeleton"); return ( <div className={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div className={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap", })} > <Checkbox id="react-loading-default-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 === "busy" ? "busy" : "skeleton") } /> </label> </div> <Loading loading={loading} variant={variant}> <article> <h3 className={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Lorem ipsum dolor sit amet </h3> <p className={css({ mb: "sm" })}> <span> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </span> </p> <p> <span> Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident. </span> </p> </article> </Loading> </div> );}
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non
proident.
<script lang="ts"> import Loading from "../Loading.svelte"; import Checkbox from "@pindoba/svelte-checkbox"; import Select from "@pindoba/svelte-select-native"; import { stack, hstack } from "@pindoba/styled-system/patterns"; import { css } from "@pindoba/styled-system/css"; let loading = $state(true); let variant = $state<"skeleton" | "busy">("skeleton"); const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" }, ];</script><div class={stack({ gap: "md", direction: "column", maxWidth: "lg" })}> <div class={hstack({ gap: "md", alignItems: "center", flexWrap: "wrap" })}> <Checkbox id="svelte-loading-default-toggle" appearance="switch" bind:checked={loading} > Loading </Checkbox> <label class={hstack({ gap: "xs", alignItems: "center" })}> <span>Variant</span> <Select items={variants} bind:value={variant} /> </label> </div> <Loading {loading} {variant}> <article> <h3 class={css({ fontSize: "xl", fontWeight: "bold", mb: "sm" })}> Lorem ipsum dolor sit amet </h3> <p class={css({ mb: "sm" })}> <span> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </span> </p> <p> <span> Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident. </span> </p> </article> </Loading></div>
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident.
<script setup lang="ts">import { ref } from "vue";import { Loading } from "@pindoba/vue-loading";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 loading = ref(true);const variant = ref<"skeleton" | "busy">("skeleton");const variants = [ { id: "skeleton", label: "Skeleton", value: "skeleton" }, { id: "busy", label: "Busy", value: "busy" },];const onToggle = (e: Event) => (loading.value = (e.target as HTMLInputElement).checked);const onVariant = (e: Event) => (variant.value = (e.target as HTMLSelectElement).value 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-loading-default-toggle" appearance="switch" :checked="loading" @change="onToggle" >Loading</Checkbox > <label :class="hstack({ gap: 'xs', alignItems: 'center' })"> <span>Variant</span> <SelectNative :items="variants" :value="variant" @change="onVariant" /> </label> </div> <Loading :loading="loading" :variant="variant"> <article> <h3 :class="css({ fontSize: 'xl', fontWeight: 'bold', mb: 'sm' })"> Lorem ipsum dolor sit amet </h3> <p :class="css({ mb: 'sm' })"> <span> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </span> </p> <p> <span> Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident. </span> </p> </article> </Loading> </div></template>
For busy regions where the shimmer alone can be subtle — short status
blocks, single rows, dense forms — pass indicator to render an animated
spinner anchored to the top-end corner. It’s rendered via Attachment so it
sits over the region without affecting its layout, and only shows while
loading is true.
busy also defaults to speed="fast" (a 1s shimmer cycle) so the motion
reads as “actively working” rather than “loading slowly”. Pass speed
explicitly to override.
Saving changes…
A short status region where the shimmer alone could be easy to miss.
The corner spinner makes the busy state unmistakable.
---import Indicator from "@pindoba/svelte-loading/demos/indicator";---<Indicator client:load />
Saving changes…
A short status region where the shimmer alone could be easy to miss. The corner spinner makes the busy state unmistakable.
import { useState } from "react";import { Loading } from "../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-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"> import Loading from "../Loading.svelte"; 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-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">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);const onToggle = (e: Event) => (loading.value = (e.target as HTMLInputElement).checked);</script><template> <div :class="stack({ gap: 'md', direction: 'column', maxWidth: 'lg' })"> <Checkbox id="vue-loading-indicator-toggle" appearance="switch" :checked="loading" @change="onToggle" >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></template>
props · 9 shown · 9 total
allAdvancedAppearanceBehaviorLayoutSlotsState
children slot svelte
Snippet
No description yet.
colorPalette
string
default neutral
Tints the skeleton shimmer using any palette token.
element binding svelte
HTMLDivElementnull
No description yet.
indicator
boolean
default false
Renders a small spinner anchored to the top-end corner of the loading
region while `loading` is true. Useful for `busy` regions whose shimmer
can be subtle on short or text-light content.
loading
boolean
default false
When true, renders children as a shimmering skeleton.
passThrough
LoadingPassThrough
Per-slot style and attribute override bag for customizing the loading
root element's styles or HTML attributes.
radius
"sm""md""pill"
default sm
Corner radius override from the spacing scale (or `full` for fully rounded).