Skip to main content

Skeleton

Skeleton is a loading placeholder that stands in for content while it loads, such as a profile or a project card.

Import

import { Skeleton } from '@vortexlabs/vortex';
import type { SkeletonProps } from '@vortexlabs/vortex';

Other links

Loading project details
"use client";

import { Button, Skeleton, Stack, Text } from "@vortexlabs/vortex";
import { useState } from "react";

export const LoadingContent = () => {
  const [loading, setLoading] = useState(true);

  return (
    <Stack gap={4} width="100%">
      <Skeleton loading={loading} ariaLabel="Loading project details">
        <Stack gap={2}>
          <Text weight="medium">Design system</Text>
          <Text>Components, patterns, and shared foundations.</Text>
        </Stack>
      </Skeleton>
      <Button variant="secondary" onClick={() => setLoading(!loading)}>
        {loading ? "Show loaded content" : "Show loading state"}
      </Button>
    </Stack>
  );
};

Features

  • Wraps loaded content and draws shapes that match its layout.
  • Builds a skeleton from sample content before data arrives.
  • Draws plain blocks at a set size.
  • Remembers loaded layouts with cacheKey.
  • Supports pulse, shimmer, and staggered animations.

Placeholder Content

Pass fixture to measure sample content when the real content isn't available yet, like in a Suspense fallback. It replaces children for measuring.

Loading project card
<Skeleton
  ariaLabel="Loading project card"
  fixture={
    <Stack gap={2}>
      <Text weight="medium">Project name</Text>
      <Text>A short project description.</Text>
    </Stack>
  }
/>

Cached Layouts

Set cacheKey to reuse the last loaded layout for content that only exists after loading. fallback shows until a layout is cached. A fixture wins over the cache, and mask ignores it.

Loading project overview
"use client";

import { Button, Skeleton, Stack, Text } from "@vortexlabs/vortex";
import { useState } from "react";

export const CachedContent = () => {
  const [loading, setLoading] = useState(true);

  return (
    <Stack gap={4} width="100%">
      <Skeleton
        loading={loading}
        cacheKey="docs-project-overview"
        ariaLabel="Loading project overview"
        fallback={<Skeleton width="60%" height={6} />}
      >
        {loading ? null : (
          <Stack gap={2}>
            <Text weight="medium">Project overview</Text>
            <Text>Track milestones and recent updates.</Text>
          </Stack>
        )}
      </Skeleton>
      <Button variant="secondary" onClick={() => setLoading(!loading)}>
        {loading ? "Show loaded project" : "Reload project"}
      </Button>
    </Stack>
  );
};

Blocks

Without children or a fixture, set width, height, and borderRadius to draw a block. Numbers use the spacing scale, strings take CSS lengths, and all three accept breakpoint objects. Blocks are fully rounded by default; set loading={false} to remove one.

<Stack direction="row" gap={6} align="center" wrap>
  <Skeleton width={14} height={14} borderRadius="full" />
  <Skeleton width="10rem" height="6rem" borderRadius="md" />
  <Skeleton width="10rem" height={6} borderRadius="none" />
</Stack>
<Skeleton
  width="100%"
  height={{ xs: "8rem", md: "12rem" }}
  borderRadius={{ xs: "sm", md: "lg" }}
/>

Variants

The default shapes variant measures the content and draws separate shapes over it. mask paints the content itself instead: text becomes bars and media and controls become blocks, without measuring.

Loading shapes example
Loading mask example
<Stack gap={6}>
  <Skeleton variant="shapes" fixture={<Text>Project details are loading.</Text>} ariaLabel="Loading shapes example" />
  <Skeleton variant="mask" fixture={<Text>Project details are loading.</Text>} ariaLabel="Loading mask example" />
</Stack>

Animation

Choose pulse (the default), shimmer, or none. Add stagger to reveal generated shapes one after another; animations respect reduced motion preferences.

<Stack gap={3}>
  <Skeleton width="60%" height={6} animation="pulse" />
  <Skeleton width="60%" height={6} animation="shimmer" />
  <Skeleton width="60%" height={6} animation="none" />
</Stack>

Shape Measurement

Use measureOptions to skip elements with ignore, draw an element as one shape with block, or turn off surfaces, the lighter shapes drawn for cards. You can also mark elements with data-vortex-skeleton="ignore" or data-vortex-skeleton="block".

Loading project summary
<Skeleton
  ariaLabel="Loading project summary"
  measureOptions={{ ignore: ["[data-project-updated]"], surfaces: false }}
  fixture={
    <Stack gap={2}>
      <Text weight="medium">Project name</Text>
      <Text>A short project description.</Text>
      <Text attributes={{ "data-project-updated": true }}>Updated today</Text>
    </Stack>
  }
/>

Content stays mounted while loading. In server-rendered HTML, filled elements keep their color until hydration; add data-vortex-skeleton="block" to mask them.

Accessibility

Description
Provide ariaLabel to describe what is loading.
Blocks without ariaLabel are silent to assistive tech. When several blocks stand in for one piece of content, give one of them an ariaLabel, not every block. With children or fixture, Skeleton hides the content and sets aria-busy itself.