Heart rate
72 beats per minute
Recovery
- Current
- 1.6 min
- Average
- 2.2 min
- Max
- 4.2 min
Choose your reading theme.
Cards drawn first as dotted blueprints, measured from the cards themselves, then swept in by a scan line one after another.
skeleton-build/skeleton-build.tsx72 beats per minute
Jul 1 – 7
1.7M Tokens
Iris Calder
Liked by Rhea Lund, Omar Faris, Mina Cho and 6 more
Building the cards
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
When not to use
You can add this component with the shadcn CLI or by hand:
Into components/skeleton-build of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/skeleton-build.jsonCopy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
import { SkeletonReveal } from './skeleton-build';export function Profile({ user }: { user?: User }) { return ( <SkeletonReveal loading={!user} onRevealed={() => console.log('shown')}> <article className="card"> <h3><span data-sk="text">{user?.name ?? 'Name Surname'}</span></h3> <img data-sk="box" data-sk-label="" src={user?.avatar} alt="" /> <button data-sk="box">Follow</button> </article> </SkeletonReveal> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
A loading state that looks like an interface being generated. Each card shows as its own blueprint (dotted runs for its words, dotted boxes and rings for its controls, its outline with corner handles, guides and sizes, over a grid of dots) while a bracket hops from part to part naming what it places. Then a dotted scan line runs down each card in turn and the real card resolves above it, soft at first and then sharp. The three sample cards (a heart-rate monitor, a stacked bar chart of token spend and a music player) are live once shown.
| Prop | Type | Default | Description |
|---|---|---|---|
loading | boolean | — | Controls the stage: true holds the blueprints, false reveals the cards. Without it, the stage builds itself and reveals on its own (SkeletonReveal requires it). |
stagger | number | 375 | Ms between one card’s reveal and the next’s. On SkeletonReveal, the same is its delay prop. |
duration | number | 350 | Ms each card’s sweep takes. |
onRevealed | () => void | — | Called once the last card is fully shown. |
defaultRevealed | number | — | Uncontrolled only: opens with this many cards shown and the rest held as blueprints, with no demo. Replay builds them all again and reveals them. |
autoplay | boolean | true | Build on its own once on screen and let a demo pointer try the cards. False shows the cards at once (Replay still builds them). |
colorScheme | 'light' | 'dark' | 'system' | 'system' | Forces a scheme, or follows the page’s color-scheme. |
className | string | — | Extra classes for the stage. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useEffectEvent, useRef, useState, type CSSProperties } from 'react';import { HeartCard, PlayerCard, TokenCard } from './cards';import { DemoPointer, runDemo, type Demo } from './demo-pointer';import { hideDelay, SkeletonReveal } from './skeleton-reveal';import { useAutoplay } from './use-autoplay';import styles from './skeleton-build.module.css';export { SkeletonReveal, type SkeletonRevealProps } from './skeleton-reveal';/* * Skeleton build: cards that are drawn first as blueprints, then swept in one after another, as * if an interface were being generated in front of the reader. * * - Each card sits in a SkeletonReveal (skeleton-reveal.tsx), which measures it and draws its * blueprint: dotted runs for its words, dotted boxes and rings for its controls, its outline with * corner handles, guides and size, over a grid of dots. On reveal, a scan line sweeps it in. * - The cards reveal `stagger` ms apart, in reading order; the last one calls onRevealed. * - Uncontrolled (no loading prop), the stage builds its blueprints for BUILD ms once it is on * screen, then reveals; Replay builds it all again. Controlled, it shows what `loading` says. * - Once shown, the cards are live: the heart rate drifts, the chart switches period, the player// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode,} from 'react';import styles from './skeleton-build.module.css';/* * Skeleton reveal: one card drawn first as its own blueprint, then swept into the real thing. * * - The card is rendered from the start, in its place, so it sets the size of the whole and nothing * moves when it appears. While loading it is clipped away to nothing. * - The blueprint is measured from the card itself: every element marked data-sk ("text", "box" or * "circle") becomes a dotted run, a dotted outline or a dotted ring at its exact place and size, * over a grid of dots, inside the card's own dotted outline with its corner handles and guides. * A text run is the width of its words, read from a Range, not of its box. Parts come in one by// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useState, type CSSProperties } from 'react';import styles from './skeleton-build.module.css';/* * The three sample cards the stage builds: a heart-rate monitor, a stacked bar chart of token * spend and a music player. All the content is invented. Every part a blueprint should draw is * marked data-sk ("text", "box" or "circle"; data-sk-label adds its size and corner handles); * the main parts also carry data-part, for the docs. * * Each card works on its own once shown: the heart rate drifts and its trace runs, the chart * switches between week, month and year and opens its details, the player plays, skips, likes * and lists who liked. `live` stops anything that runs on a clock (off screen, hidden tab, or * while the card is still a blueprint). *//* Heart rate ---------------------------------------------------------------------------------- *//** The readings the monitor drifts through, one every BEAT ms. */const READINGS = [72, 74, 77, 76, 73, 75];const BEAT = 1900;// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it (anything that * plays its own animation on the same clock). The arrow and the hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. It also tells whether the element is on screen * in a visible tab, and whether the reader prefers reduced motion. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden);/* Skeleton build (skeleton-build.tsx, skeleton-reveal.tsx, cards.tsx): a pale stage with three cards in a row (stacked when narrow), each drawn first as a dotted blueprint and then swept in. Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it inherits from the page (light unless the page says otherwise), or the one its colorScheme prop sets (data-color-scheme). Memory. The blueprint is one small SVG per card plus a CSS grid of dots; the sweep moves one element by a transform while the card's clip-path opens on the same clock. Nothing runs frame by frame in JavaScript, and every animation rests off screen (data-paused). */.root { /* The stage, the cards on it, and the cards' quieter surfaces. */ --stage: light-dark(#fafafa, #111112); --card: light-dark(#fff, #1c1c1e); --soft: light-dark(#f2f2f3, #2a2a2d); --chart: light-dark(#f7f7f8, #232326); --grid-line: light-dark(#e9e9eb, #313134); --rule: light-dark(#efefef, #2c2c2f); /* Words: main, secondary and faint. */ --ink: light-dark(#141414, #f3f3f3); --muted: light-dark(#7b7b7e, #9c9ca1); --faint: light-dark(#a3a3a6, #75757a); /* The cards' edge and lift. */