Monet
@etched/monet 0.0.1

Loader

Ported

Spinner that tells screen readers what is loading (role="status"). Its own SVG, not an icon: a faint track and an arc that grows and turns, in CSS only. With delay, the status is there from the start but empty and without a name; the spinner, its text and its name come in after the delay, so they are announced then.

Loading runs
import { Loader } from "@etched/monet/loader";

export function Preview() {
  return <Loader aria-label="Loading runs" />;
}

Installation

import { Loader, Skeleton, SkeletonLine } from "@etched/monet/loader";

Examples

Sizes

Three presets, or a size in px where they do not fit.

LoadingLoadingLoadingLoading, 48 px
import { Loader, type LoaderSize } from "@etched/monet/loader";

const sizes: LoaderSize[] = ["sm", "base", "lg"];

export function Sizes() {
  return (
    <div className="row">
      {sizes.map((s) => (
        <Loader key={s} size={s} />
      ))}
      <Loader size={48} aria-label="Loading, 48 px" />
    </div>
  );
}

Delay

delay waits before the spinner shows and is announced, so a load that ends quickly does not flash it. Its room is kept, so nothing moves.

import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Loader } from "@etched/monet/loader";

export function Delay() {
  const [runs, setRuns] = useState(0);

  return (
    <div className="row">
      <Button onClick={() => setRuns(runs + 1)}>Load again</Button>
      <Loader key={runs} delay={400} aria-label="Loading after a delay" />
    </div>
  );
}

Inline, with its own label

Chip SH-24A-0193 Loading chip status is being checked.

import { Loader } from "@etched/monet/loader";

export function InlineWithItsOwnLabel() {
  return (
    <p>
      Chip SH-24A-0193 <Loader size="sm" aria-label="Loading chip status" /> is being checked.
    </p>
  );
}

Page

page fills the space it is given and centers the spinner, for example as a Suspense fallback.

Loading referrals
import { Loader } from "@etched/monet/loader";

const frame = { display: "flex", blockSize: "14rem", borderRadius: "var(--et-radius-lg)", boxShadow: "inset 0 0 0 1px var(--et-line)" };

export function Page() {
  return (
    <div style={frame}>
      <Loader page aria-label="Loading referrals" />
    </div>
  );
}

Skeleton lines

A line is as tall as a line of the text around it, so the layout does not move when the text arrives.

Owner
import { SkeletonLine } from "@etched/monet/loader";

const ring = { borderRadius: "var(--et-radius-lg)", boxShadow: "inset 0 0 0 1px var(--et-line)", paddingInline: "var(--et-space-3)" };

export function SkeletonLines() {
  return (
    <div className="grid">
      <div>
        <div style={{ fontSize: "var(--et-text-heading)", lineHeight: "var(--et-leading-heading)", inlineSize: "40%" }}>
          <SkeletonLine />
        </div>
        <SkeletonLine />
        <SkeletonLine />
        <SkeletonLine style={{ inlineSize: "60%" }} />
      </div>
      <div className="row" style={{ flexWrap: "nowrap" }}>
        <span>Owner</span>
        <SkeletonLine style={{ inlineSize: "8rem" }} />
      </div>
      <div style={ring}>
        <SkeletonLine style={{ blockSize: "var(--et-control-base)" }} />
      </div>
    </div>
  );
}

Skeleton shapes

Skeleton stands in for a block (a card or a chart), a circle (an avatar) or a paragraph (lines, the last one shorter).

import { Skeleton, SkeletonLine } from "@etched/monet/loader";

export function SkeletonShapes() {
  return (
    <div className="grid" aria-busy="true">
      <div className="row" style={{ flexWrap: "nowrap", alignItems: "start", gap: "var(--et-space-3)" }}>
        <Skeleton shape="circle" />
        <div style={{ flex: 1 }}>
          <SkeletonLine style={{ inlineSize: "40%" }} />
          <Skeleton shape="text" lines={2} />
        </div>
      </div>
      <Skeleton style={{ blockSize: "8rem" }} />
      <Skeleton shape="text" />
    </div>
  );
}

Accessibility

  • Loader is a status named by aria-label (default "Loading"). The spinner is hidden; visually hidden text holds the same words.
  • With delay, the status is there but empty and unnamed. The spinner and its text come in after the delay, so screen readers hear them then.
  • Skeleton and SkeletonLine have aria-hidden, so screen readers do not hear them.
  • Pass a translated label that says what loads, such as "Loading runs". With skeletons, mark the loading region aria-busy or add a Loader.

API reference

Loader

PropTypeDefaultDescription
sizeLoaderSize | number"base", or "lg" with `page`A preset, or a size in px for a place the presets do not fit.
aria-labelstring"Loading"Names the status, and screen readers announce it. Pass translated text.
pageboolean–Fill the space it is given and center the spinner. For a page or panel that loads, for example a Suspense fallback.
delaynumber0Milliseconds to wait before the spinner shows and is announced, so a fast load does not flash it. 300 to 500 suits most loads.

Skeleton

PropTypeDefaultDescription
shape"block" | "circle" | "text""block"
linesnumber3How many lines shape="text" shows.