Skip to main content

Skeleton

Shimmer placeholders that mimic real content while data loads.

Quick start
import { Skeleton } from "mates-ui";

Skeleton({ variant: "text", width: "60%", height: "0.88rem" });

Variants

import { html, renderApp } from 'mates';
import { Skeleton, Text } from 'mates-ui';

const App = () => () => html`
<x-col gap="var(--md-space-4)" style="max-width:22.5rem;width:100%;">
<x-col gap="var(--md-space-2)">
${Text("rect", { type: "label-sm", color: "muted" })}
${Skeleton({ variant: "rect", width: "100%", height: "5rem" })}
</x-col>
<x-col gap="var(--md-space-2)">
${Text("text — 1 line", { type: "label-sm", color: "muted" })}
${Skeleton({ variant: "text", width: "80%", height: "0.88rem" })}
</x-col>
<x-col gap="var(--md-space-2)">
${Text("text — 4 lines", { type: "label-sm", color: "muted" })}
${Skeleton({ variant: "text", lines: 4, height: "0.88rem" })}
</x-col>
<x-col gap="var(--md-space-2)">
${Text("circle", { type: "label-sm", color: "muted" })}
<x-row gap="var(--md-space-3)" align="center">
${Skeleton({ variant: "circle", width: "2rem" })}
${Skeleton({ variant: "circle", width: "2.5rem" })}
${Skeleton({ variant: "circle", width: "3.5rem" })}
</x-row>
</x-col>
</x-col>
`;

renderApp(App, document.getElementById('app'));

Multi-line text

import { html, renderApp } from 'mates';
import { Skeleton } from 'mates-ui';

const App = () => () => html`
<x-col gap="var(--md-space-4)" style="max-width:22.5rem;width:100%;">
${Skeleton({ variant: "text", lines: 2, height: "0.88rem" })}
${Skeleton({ variant: "text", lines: 5, height: "0.88rem" })}
${Skeleton({ variant: "text", lines: 8, height: "0.88rem" })}
</x-col>
`;

renderApp(App, document.getElementById('app'));

Profile card skeleton

import { html, renderApp } from 'mates';
import { Skeleton } from 'mates-ui';

const App = () => () => html`
<div style="display:flex;gap:var(--md-space-4);align-items:flex-start;
padding:var(--md-space-5);border-radius:var(--md-radius-lg);
background:var(--md-color-surface-raised);max-width:25rem;
box-shadow:var(--md-shadow-1);">
${Skeleton({ variant: "circle", width: "3rem" })}
<div style="display:flex;flex-direction:column;gap:var(--md-space-2);flex:1;">
${Skeleton({ variant: "text", width: "55%", height: "1rem" })}
${Skeleton({ variant: "text", width: "35%", height: "0.75rem" })}
<div style="margin-top:var(--md-space-2);">
${Skeleton({ variant: "text", lines: 3, height: "0.81rem" })}
</div>
<div style="display:flex;gap:var(--md-space-2);margin-top:var(--md-space-2);">
${Skeleton({ variant: "rect", width: "4.5rem", height: "2rem" })}
${Skeleton({ variant: "rect", width: "4.5rem", height: "2rem" })}
</div>
</div>
</div>
`;

renderApp(App, document.getElementById('app'));

Loading vs loaded comparison

import { html, atom, renderApp } from 'mates';
import { Avatar, Skeleton, Text, FilledButton } from 'mates-ui';

const App = () => {
const loaded = atom(false);
return () => html`
<x-col gap="var(--md-space-4)" style="max-width:22rem;width:100%;">
${FilledButton(loaded() ? "Show skeleton" : "Show loaded", {
size: "sm",
onClick: () => loaded.set((v) => !v),
})}
<div style="padding:var(--md-space-4);border-radius:var(--md-radius-lg);
background:var(--md-color-surface-raised);box-shadow:var(--md-shadow-1);">
${loaded() ? html`
<div style="display:flex;gap:var(--md-space-3);align-items:center;margin-bottom:var(--md-space-3);">
${Avatar({ name: "Alex Kim", size: "sm" })}
<div>
${Text("Alex Kim", { type: "label-md" })}
${Text("Product Designer", { type: "label-sm", color: "muted" })}
</div>
</div>
${Text("Working on design systems and component libraries.", { type: "body-sm", color: "muted" })}
` : html`
<div style="display:flex;gap:var(--md-space-3);align-items:center;margin-bottom:var(--md-space-3);">
${Skeleton({ variant: "circle", width: "2.25rem" })}
<div style="display:flex;flex-direction:column;gap:var(--md-space-1);flex:1;">
${Skeleton({ variant: "text", width: "60%", height: "0.81rem" })}
${Skeleton({ variant: "text", width: "40%", height: "0.69rem" })}
</div>
</div>
${Skeleton({ variant: "text", lines: 2, height: "0.81rem" })}
`}
</div>
</x-col>
`;
};

Feed / list skeleton

import { html, renderApp } from 'mates';
import { Skeleton } from 'mates-ui';

const App = () => () => html`
<x-col gap="var(--md-space-4)" style="max-width:26rem;width:100%;">
${[1, 2, 3].map(() => html`
<div style="display:flex;gap:var(--md-space-3);align-items:center;">
${Skeleton({ variant: "circle", width: "2.5rem" })}
<div style="display:flex;flex-direction:column;gap:var(--md-space-2);flex:1;">
${Skeleton({ variant: "text", width: "50%", height: "0.88rem" })}
${Skeleton({ variant: "text", width: "75%", height: "0.75rem" })}
</div>
${Skeleton({ variant: "rect", width: "3.5rem", height: "1.25rem" })}
</div>
`)}
</x-col>
`;

renderApp(App, document.getElementById('app'));

Table skeleton

import { html, renderApp } from 'mates';
import { Skeleton } from 'mates-ui';

const App = () => () => html`
<x-col gap="var(--md-space-2)" style="max-width:32rem;width:100%;">
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:var(--md-space-3);padding:var(--md-space-2) 0;">
${Skeleton({ variant: "rect", width: "100%", height: "0.75rem" })}
${Skeleton({ variant: "rect", width: "100%", height: "0.75rem" })}
${Skeleton({ variant: "rect", width: "100%", height: "0.75rem" })}
</div>
${[1, 2, 3, 4].map(() => html`
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:var(--md-space-3);
padding:var(--md-space-2) 0;border-top:0.06rem solid var(--md-color-divider);">
${Skeleton({ variant: "text", width: "100%", height: "0.88rem" })}
${Skeleton({ variant: "text", width: "80%", height: "0.88rem" })}
${Skeleton({ variant: "rect", width: "3.25rem", height: "1.38rem" })}
</div>
`)}
</x-col>
`;

renderApp(App, document.getElementById('app'));

Skeleton

import { Skeleton } from "mates-ui";
Prop Type Default Description
attr AttrMap Extra HTML attributes forwarded to the root element.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
height string "1rem" CSS height of the skeleton element.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
lines number 1 For `variant: "text"` — renders multiple stacked lines. The last line renders at ~70% width.
on OnEventMap DOM event handlers on the root element.
style StyleMap Root inline CSS (`style` prop).
style StyleMap Inline CSS on the root element.
testId string Selector for testing (data-testid attribute).
variant "text" | "rect" | "circle" "rect" Shape of the skeleton placeholder.
width string "100%" CSS width of the skeleton element.

CSS tokens

Token Role
--md-color-surface-raised Shimmer highlight color
--md-color-surface-variant Shimmer base color

Caveats

  • `lines` only applies to `variant: "text"` — the last line renders at ~70% width.
  • `width` on `variant: "circle"` sets both width and height (it's always square).
  • Skeleton does not animate in reduced-motion environments — `prefers-reduced-motion` is respected.