Skeleton
Reserve the shape of incoming content without announcing each placeholder. Text follows the surrounding line metrics; circles and rectangles stand in for avatars and media. Use one status message and aria-busy on the loading region, then replace the placeholders with real content. Keep existing content visible during refresh when possible. A broad shimmer sweeps across the placeholders together by default, continuously from left to right. Reduced-motion preferences always keep it static.
Loading text
Placeholders reserve real line boxes while content loads.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-skeleton/ui-skeleton.html"
>
<ui-stack
gap="xs"
>
<ui-skeleton></ui-skeleton>
<ui-skeleton
width="85%"
></ui-skeleton>
<ui-skeleton
width="60%"
></ui-skeleton>
</ui-stack><script setup>
import { UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="xs"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>
</template>import { UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="xs"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>;
}/** @jsxImportSource preact */
import { UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="xs"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>;
}<script>
import { UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="xs"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>import { Component } from "@angular/core";
import { UiSkeleton, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiSkeleton],
template: `
<div
ui-stack
[gap]="'xs'"
>
<span
ui-skeleton
></span>
<span
ui-skeleton
[width]="'85%'"
></span>
<span
ui-skeleton
[width]="'60%'"
></span>
</div>
`,
})
export default class Example {}Text lines
Each placeholder reserves a whole text line. Vary the widths to suggest the content without pretending to know its words.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-skeleton/ui-skeleton.html"
>
<ui-stack
gap="none"
>
<ui-skeleton></ui-skeleton>
<ui-skeleton
width="85%"
></ui-skeleton>
<ui-skeleton
width="60%"
></ui-skeleton>
</ui-stack><script setup>
import { UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>
</template>import { UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>;
}/** @jsxImportSource preact */
import { UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>;
}<script>
import { UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="85%"
/>
<UiSkeleton
width="60%"
/>
</UiStack>import { Component } from "@angular/core";
import { UiSkeleton, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiSkeleton],
template: `
<div
ui-stack
[gap]="'none'"
>
<span
ui-skeleton
></span>
<span
ui-skeleton
[width]="'85%'"
></span>
<span
ui-skeleton
[width]="'60%'"
></span>
</div>
`,
})
export default class Example {}Avatar and media shapes
The default circle matches a medium Avatar. Supply dimensions for a thumbnail or a larger media area; narrow containers constrain the width.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-skeleton/ui-skeleton.html"
>
<ui-stack
gap="m"
>
<ui-cluster
gap="m"
>
<ui-skeleton
shape="circle"
></ui-skeleton>
<ui-skeleton
shape="circle"
width="1.75rem"
></ui-skeleton>
<ui-skeleton
shape="rectangle"
width="6rem"
height="2.5rem"
></ui-skeleton>
</ui-cluster>
<ui-skeleton
shape="rectangle"
></ui-skeleton>
</ui-stack><script setup>
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="m"
>
<UiCluster
gap="m"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
shape="circle"
width="1.75rem"
/>
<UiSkeleton
shape="rectangle"
width="6rem"
height="2.5rem"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
/>
</UiStack>
</template>import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiCluster
gap="m"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
shape="circle"
width="1.75rem"
/>
<UiSkeleton
shape="rectangle"
width="6rem"
height="2.5rem"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
/>
</UiStack>;
}/** @jsxImportSource preact */
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiCluster
gap="m"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
shape="circle"
width="1.75rem"
/>
<UiSkeleton
shape="rectangle"
width="6rem"
height="2.5rem"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
/>
</UiStack>;
}<script>
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="m"
>
<UiCluster
gap="m"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
shape="circle"
width="1.75rem"
/>
<UiSkeleton
shape="rectangle"
width="6rem"
height="2.5rem"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
/>
</UiStack>import { Component } from "@angular/core";
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiCluster, UiSkeleton],
template: `
<div
ui-stack
[gap]="'m'"
>
<div
ui-cluster
[gap]="'m'"
>
<span
ui-skeleton
[shape]="'circle'"
></span>
<span
ui-skeleton
[shape]="'circle'"
[width]="'1.75rem'"
></span>
<span
ui-skeleton
[shape]="'rectangle'"
[width]="'6rem'"
[height]="'2.5rem'"
></span>
</div>
<span
ui-skeleton
[shape]="'rectangle'"
></span>
</div>
`,
})
export default class Example {}Loading a profile
One status describes the work; the section stays busy until the application replaces its decorative placeholders. Compare the loaded card's avatar and text metrics.
Ada Lovelace
Mathematician
Notes on an analytical engine and the possibilities of computation.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-status-message/ui-status-message.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-grid/ui-grid.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-card/ui-card.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-skeleton/ui-skeleton.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-avatar/ui-avatar.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<ui-stack
gap="m"
>
<ui-status-message
kind="loading"
>Loading a profile…</ui-status-message>
<ui-grid>
<ui-card
as="section"
aria-label="Profile"
aria-busy="true"
>
<ui-cluster
gap="s"
>
<ui-skeleton
shape="circle"
></ui-skeleton>
<ui-stack
fill
gap="none"
>
<ui-skeleton
width="65%"
></ui-skeleton>
<ui-skeleton
width="45%"
></ui-skeleton>
</ui-stack>
</ui-cluster>
<ui-stack
gap="none"
>
<ui-skeleton></ui-skeleton>
<ui-skeleton
width="80%"
></ui-skeleton>
</ui-stack>
</ui-card>
<ui-card
as="section"
aria-label="Loaded profile"
>
<ui-cluster
gap="s"
>
<ui-avatar
name="Ada Lovelace"
decorative
></ui-avatar>
<ui-stack
fill
gap="none"
>
<ui-text
weight="semibold"
>Ada Lovelace</ui-text>
<ui-text
tone="secondary"
>Mathematician</ui-text>
</ui-stack>
</ui-cluster>
<ui-text>Notes on an analytical engine and the possibilities of computation.</ui-text>
</ui-card>
</ui-grid>
</ui-stack><script setup>
import {
UiAvatar,
UiCard,
UiCluster,
UiGrid,
UiSkeleton,
UiStack,
UiStatusMessage,
UiText,
} from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="m"
>
<UiStatusMessage
kind="loading"
>Loading a profile…</UiStatusMessage>
<UiGrid>
<UiCard
as="section"
aria-label="Profile"
aria-busy="true"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiStack
:fill="true"
gap="none"
>
<UiSkeleton
width="65%"
/>
<UiSkeleton
width="45%"
/>
</UiStack>
</UiCluster>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="80%"
/>
</UiStack>
</UiCard>
<UiCard
as="section"
aria-label="Loaded profile"
>
<UiCluster
gap="s"
>
<UiAvatar
name="Ada Lovelace"
:decorative="true"
/>
<UiStack
:fill="true"
gap="none"
>
<UiText
weight="semibold"
>Ada Lovelace</UiText>
<UiText
tone="secondary"
>Mathematician</UiText>
</UiStack>
</UiCluster>
<UiText>Notes on an analytical engine and the possibilities of computation.</UiText>
</UiCard>
</UiGrid>
</UiStack>
</template>import {
UiAvatar,
UiCard,
UiCluster,
UiGrid,
UiSkeleton,
UiStack,
UiStatusMessage,
UiText,
} from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiStatusMessage
kind="loading"
>Loading a profile…</UiStatusMessage>
<UiGrid>
<UiCard
as="section"
aria-label="Profile"
aria-busy="true"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiStack
fill
gap="none"
>
<UiSkeleton
width="65%"
/>
<UiSkeleton
width="45%"
/>
</UiStack>
</UiCluster>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="80%"
/>
</UiStack>
</UiCard>
<UiCard
as="section"
aria-label="Loaded profile"
>
<UiCluster
gap="s"
>
<UiAvatar
name="Ada Lovelace"
decorative
/>
<UiStack
fill
gap="none"
>
<UiText
weight="semibold"
>Ada Lovelace</UiText>
<UiText
tone="secondary"
>Mathematician</UiText>
</UiStack>
</UiCluster>
<UiText>Notes on an analytical engine and the possibilities of computation.</UiText>
</UiCard>
</UiGrid>
</UiStack>;
}/** @jsxImportSource preact */
import {
UiAvatar,
UiCard,
UiCluster,
UiGrid,
UiSkeleton,
UiStack,
UiStatusMessage,
UiText,
} from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiStatusMessage
kind="loading"
>Loading a profile…</UiStatusMessage>
<UiGrid>
<UiCard
as="section"
aria-label="Profile"
aria-busy="true"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiStack
fill
gap="none"
>
<UiSkeleton
width="65%"
/>
<UiSkeleton
width="45%"
/>
</UiStack>
</UiCluster>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="80%"
/>
</UiStack>
</UiCard>
<UiCard
as="section"
aria-label="Loaded profile"
>
<UiCluster
gap="s"
>
<UiAvatar
name="Ada Lovelace"
decorative
/>
<UiStack
fill
gap="none"
>
<UiText
weight="semibold"
>Ada Lovelace</UiText>
<UiText
tone="secondary"
>Mathematician</UiText>
</UiStack>
</UiCluster>
<UiText>Notes on an analytical engine and the possibilities of computation.</UiText>
</UiCard>
</UiGrid>
</UiStack>;
}<script>
import {
UiAvatar,
UiCard,
UiCluster,
UiGrid,
UiSkeleton,
UiStack,
UiStatusMessage,
UiText,
} from "@threadlabs/looma";
</script>
<UiStack
gap="m"
>
<UiStatusMessage
kind="loading"
>Loading a profile…</UiStatusMessage>
<UiGrid>
<UiCard
as="section"
aria-label="Profile"
aria-busy="true"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiStack
fill
gap="none"
>
<UiSkeleton
width="65%"
/>
<UiSkeleton
width="45%"
/>
</UiStack>
</UiCluster>
<UiStack
gap="none"
>
<UiSkeleton />
<UiSkeleton
width="80%"
/>
</UiStack>
</UiCard>
<UiCard
as="section"
aria-label="Loaded profile"
>
<UiCluster
gap="s"
>
<UiAvatar
name="Ada Lovelace"
decorative
/>
<UiStack
fill
gap="none"
>
<UiText
weight="semibold"
>Ada Lovelace</UiText>
<UiText
tone="secondary"
>Mathematician</UiText>
</UiStack>
</UiCluster>
<UiText>Notes on an analytical engine and the possibilities of computation.</UiText>
</UiCard>
</UiGrid>
</UiStack>import { Component } from "@angular/core";
import {
UiAvatar,
UiCard,
UiCluster,
UiGrid,
UiSkeleton,
UiStack,
UiStatusMessage,
UiText,
} from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiStatusMessage, UiGrid, UiCard, UiCluster, UiSkeleton, UiAvatar, UiText],
template: `
<div
ui-stack
[gap]="'m'"
>
<div
ui-status-message
[kind]="'loading'"
>Loading a profile…</div>
<div
ui-grid
>
<ng-template
ui-card
[as]="'section'"
[hnAttributes]="{ 'aria-label': 'Profile', 'aria-busy': 'true' }"
>
<div
ui-cluster
[gap]="'s'"
>
<span
ui-skeleton
[shape]="'circle'"
></span>
<div
ui-stack
[fill]="true"
[gap]="'none'"
>
<span
ui-skeleton
[width]="'65%'"
></span>
<span
ui-skeleton
[width]="'45%'"
></span>
</div>
</div>
<div
ui-stack
[gap]="'none'"
>
<span
ui-skeleton
></span>
<span
ui-skeleton
[width]="'80%'"
></span>
</div>
</ng-template>
<ng-template
ui-card
[as]="'section'"
[hnAttributes]="{ 'aria-label': 'Loaded profile' }"
>
<div
ui-cluster
[gap]="'s'"
>
<span
ui-avatar
[name]="'Ada Lovelace'"
[decorative]="true"
></span>
<div
ui-stack
[fill]="true"
[gap]="'none'"
>
<ng-template
ui-text
[weight]="'semibold'"
>Ada Lovelace</ng-template>
<ng-template
ui-text
[tone]="'secondary'"
>Mathematician</ng-template>
</div>
</div>
<ng-template
ui-text
>Notes on an analytical engine and the possibilities of computation.</ng-template>
</ng-template>
</div>
</div>
`,
})
export default class Example {}Shared shimmer
One broad gradient stays fixed to the viewport, so every placeholder reveals the same moving highlight. It loops continuously and stops for reduced motion.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-skeleton/ui-skeleton.html"
>
<ui-stack
gap="s"
>
<ui-cluster
gap="s"
>
<ui-skeleton
shape="circle"
></ui-skeleton>
<ui-skeleton
width="50%"
></ui-skeleton>
</ui-cluster>
<ui-skeleton
shape="rectangle"
width="75%"
height="3rem"
></ui-skeleton>
</ui-stack><script setup>
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="s"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
width="50%"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
width="75%"
height="3rem"
/>
</UiStack>
</template>import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
width="50%"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
width="75%"
height="3rem"
/>
</UiStack>;
}/** @jsxImportSource preact */
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
width="50%"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
width="75%"
height="3rem"
/>
</UiStack>;
}<script>
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="s"
>
<UiCluster
gap="s"
>
<UiSkeleton
shape="circle"
/>
<UiSkeleton
width="50%"
/>
</UiCluster>
<UiSkeleton
shape="rectangle"
width="75%"
height="3rem"
/>
</UiStack>import { Component } from "@angular/core";
import { UiCluster, UiSkeleton, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiCluster, UiSkeleton],
template: `
<div
ui-stack
[gap]="'s'"
>
<div
ui-cluster
[gap]="'s'"
>
<span
ui-skeleton
[shape]="'circle'"
></span>
<span
ui-skeleton
[width]="'50%'"
></span>
</div>
<span
ui-skeleton
[shape]="'rectangle'"
[width]="'75%'"
[height]="'3rem'"
></span>
</div>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: span
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
animated | Moves a subtle highlight from left to right. On by default; pass false to keep it static. Reduced-motion preferences always disable the shimmer. | animated | boolean | true | — |
height | Block size as a CSS length. Empty reserves one line for text, a square for circles, or 8rem for rectangles. Set only width to keep a circle square. | height | string | | — |
shape | The content's silhouette. text reserves one inherited line box with a shorter bar inside it; circle matches a medium Avatar; rectangle reserves a media area. | shape | keyword | text | text, circle, rectangle |
width | Inline size as a CSS length or percentage, constrained to the container. Empty fills the available width for text and rectangles, or matches a medium Avatar's outer size for circles. | width | string | | — |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
animated | boolean | true | — |
height | string | | — |
shape | "text" | "circle" | "rectangle" | text | text, circle, rectangle |
width | string | | — |
Events
No events.
Slots
No slots.
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
| Token | Declared values | Fallback |
|---|---|---|
--ui-skeleton-height | — | 1lh | auto | 8rem |
--ui-skeleton-width | — | 100% | calc(2.5rem + 2 * var(--ui-border-width)) |
Shared tokens
| Token | Fallback |
|---|---|
--ui-border-width | — |
--ui-radius-md | — |
--ui-radius-round | — |
--ui-radius-sm | — |
--ui-text-primary | — |