Skeleton

Grey shapes that stand where content is still loading and hold the height it will have (also called a placeholder or shimmer). Reach for it when you know the layout of what is coming, so nothing jumps when it arrives.

import { Skeleton } from "@umriss-ui/core";

A skeleton is hidden from screen readers. Mark the region that is loading with aria-busy, and say in words what loads where that matters.

Examples

Lines of text to come

A line per line of text, the last one shorter, so the placeholder has the height the text will have.

import { Skeleton, Stack } from "@umriss-ui/core";

export default function Lines() {
  return (
    <Stack gap={2} style={{ maxWidth: 420 }}>
      <Skeleton />
      <Skeleton />
      <Skeleton width="60%" />
    </Stack>
  );
}

Shapes

circle, width and height give it the shape of what comes – an avatar, a name, a line beneath it.

import { Skeleton, Stack } from "@umriss-ui/core";

export default function Shapes() {
  return (
    <Stack direction="row" gap={3} align="center" style={{ maxWidth: 420 }}>
      <Skeleton circle width={32} height={32} />
      <Stack gap={2} style={{ flex: 1 }}>
        <Skeleton width="55%" height={14} />
        <Skeleton width="35%" height={10} />
      </Stack>
    </Stack>
  );
}

Hold a list's place while it loads

The placeholder rows are as tall as the rows that replace them, so nothing below jumps when the data arrives.

import { useState } from "react";
import { Button, Skeleton, Stack, Text } from "@umriss-ui/core";

const PEOPLE = [
  { name: "Maya Lindgren", role: "Product manager · Web" },
  { name: "Arjun Mehta", role: "Developer · Web" },
  { name: "Noah Fischer", role: "Designer · Web" },
];

function Avatar({ name }: { name: string }) {
  return (
    <span
      aria-hidden="true"
      style={{
        width: 32,
        height: 32,
        borderRadius: "50%",
        display: "grid",
        placeItems: "center",
        background: "var(--u-color-surface-sunken)",
        fontSize: "var(--u-text-xs)",
      }}
    >
      {name.split(" ").map((part) => part[0])}
    </span>
  );
}

export default function AListLoading() {
  const [loading, setLoading] = useState(true);

  return (
    <Stack gap={4} style={{ maxWidth: 420 }}>
      <Stack gap={3} aria-busy={loading}>
        {PEOPLE.map((person) => (
          <Stack key={person.name} direction="row" gap={3} align="center" style={{ minHeight: 36 }}>
            {loading ? (
              <>
                <Skeleton circle width={32} height={32} />
                <Stack gap={2} style={{ flex: 1 }}>
                  <Skeleton width="50%" height={14} />
                  <Skeleton width="35%" height={10} />
                </Stack>
              </>
            ) : (
              <>
                <Avatar name={person.name} />
                <Stack gap={1}>
                  <Text size="sm" weight="medium">
                    {person.name}
                  </Text>
                  <Text size="xs" tone="muted">
                    {person.role}
                  </Text>
                </Stack>
              </>
            )}
          </Stack>
        ))}
      </Stack>
      <Button size="sm" onClick={() => setLoading((l) => !l)} style={{ alignSelf: "flex-start" }}>
        {loading ? "Show the loaded rows" : "Show the placeholders"}
      </Button>
    </Stack>
  );
}

When to use something else

Keyboard

The keys of Button apply here.

Accessibility

Every skeleton is aria-hidden: a screen reader meets nothing where it stands, and nothing is announced when the content replaces it. The region that loads carries aria-busy from you, and a word where the wait matters.

With reduced motion the shimmer stops, and the shape stands in a still, faint grey.

API

SkeletonProps

NameTypeDefaultDescription
widthstring | number"100%"Width, e.g. "120px" or "60%".

Shown in: Configurator, Lines of text to come, Shapes and 2 more

heightstring | number"12px"Height, e.g. "12px" or 16.

Shown in: Configurator, Shapes, Hold a list's place while it loads

circlebooleanfalseCircular shape, e.g. for an avatar placeholder.

Shown in: Configurator, Shapes, Hold a list's place while it loads

refRef<HTMLSpanElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of <span>.

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index