AZMARA PLATFORM
Components

Loading

Spinners, loading messages, logo loaders, overlays for refetching data and a top-of-page loading bar. The message and spinner are Server Components and fine in loading.tsx.

View as Markdown for coding assistants, or see llms.txt.

Usage

import { Loading, Spinner } from "@azmr/ui/loading"; // Server Components
import { LoadingBar, LoadingOverlay, useLoadingState } from "@azmr/ui/loading/client";
Preview theme
Preview density
Loading…Loading dataLoading invoices…
<div className="flex flex-col gap-6">
  <div className="flex flex-wrap items-center gap-6">
    <Spinner size="xs" />
    <Spinner size="sm" />
    <Spinner size="md" tone="default" />
    <Spinner size="lg" tone="success" />
    <Spinner size="xl" tone="info" />
  </div>
  <div className="flex flex-wrap items-center gap-8">
    <Loading />
    <Loading label="Loading data" tone="default" />
    <Loading label="Loading invoices…" labelPlacement="bottom" size="md" tone="default" />
  </div>
</div>
Open as text

Spinner is the bare ring: size from xs to xl (or inherit, 1em, for inside text and buttons) and tone for its colour. Loading adds a message beside or below it (labelPlacement) in a role="status" region, so it is shown and announced. Every part takes className and style, merged last, so any default can be overridden.

Preview theme
Preview density
LoadingLoading dataStarting up
<div className="flex flex-wrap items-end gap-10">
  {/* The ring spins; the logo stays upright and readable. */}
  <Loading logo={<Logo />} label="Loading" labelPlacement="bottom" tone="default" />
  <Loading logo={<Logo />} size="xl" label="Loading data" labelPlacement="bottom" />
  {/* No ring: the logo pulses. */}
  <Loading
    logo={<Logo />}
    animation="pulse"
    size="xl"
    label="Starting up"
    labelPlacement="bottom"
  />
</div>
Open as text

Pass logo (an <img>, an <svg> or an Avatar) and a ring spins around it while the logo itself stays upright, so it is always readable. animation="pulse" drops the ring and fades the logo in and out instead. The logo is decorative; the message is what gets announced, so give images alt="".

Pages and sections

variant="page" fills the content area and variant="section" fills its container. Both put the message under the spinner and wait 200 ms before fading in, so a fast load shows nothing at all. The delay is CSS (animation-delay on opacity), so it works with no client JavaScript.

app/(portal)/invoices/loading.tsx
import { Loading } from "@azmr/ui/loading";

export default function InvoicesLoading() {
  return <Loading variant="page" label="Loading invoices…" />;
}
A slow part of a page
import { Suspense } from "react";
import { Loading } from "@azmr/ui/loading";

<Suspense fallback={<Loading variant="section" label="Loading recent payments…" />}>
  <RecentPayments />
</Suspense>

delay={0} shows it at once; any other number of milliseconds sets the wait.

Refetching data that is already on screen

Preview theme
Preview density
  • INV-2026-001$1,150.00
  • INV-2026-002$460.00
  • INV-2026-003$89.70
<div className="flex max-w-md flex-col gap-4">
  <Button onClick={refresh} disabled={refreshing} className="self-start">
    Refresh invoices
  </Button>
  <LoadingOverlay active={refreshing} label="Refreshing invoices" className="rounded-md border">
    <ul className="divide-y text-sm">
      <li className="flex justify-between p-3">
        <span>INV-2026-001</span>
        <span>$1,150.00</span>
      </li>
      <li className="flex justify-between p-3">
        <span>INV-2026-002</span>
        <span>$460.00</span>
      </li>
      <li className="flex justify-between p-3">
        <span>INV-2026-003</span>
        <span>$89.70</span>
      </li>
    </ul>
  </LoadingOverlay>
</div>
Open as text

When data is already showing and is being refreshed, replacing it with a spinner throws away what the user was reading. LoadingOverlay keeps it: the content dims, the region gets aria-busy, and a spinner with the message sits on top.

It shows only once active has been true for delay (default 200 ms), and then stays for at least minDuration (default 400 ms), so a quick refetch never flickers. The same timing is available on its own as useLoadingState(loading, { delay, minDuration }), which returns whether to show your own indicator.

Top-of-page loading bar

Preview theme
Preview density
// `absolute` keeps the demo's bar inside this frame; in an app it is fixed to the top of the page.
<div className="relative flex gap-3 overflow-hidden rounded-md border p-6 pt-8">
  <LoadingBar loading={loading} value={value} className="absolute" />
  <Button onClick={navigate} disabled={loading}>
    Go to invoices
  </Button>
  <Button variant="outline" onClick={upload} disabled={loading}>
    Upload (known progress)
  </Button>
</div>
Open as text

LoadingBar is a thin bar fixed to the top of the page, like GitHub's. While loading is true it creeps towards 90% and slows as it gets there; when loading turns false it fills to 100% and fades away. A load faster than delay (default 150 ms) shows nothing. Pass value (0 to 100) when the real progress is known, and it shows that instead.

It works with any router: drive loading from your own pending state. In a Next.js App Router app, wrap navigations or Server Actions in a transition and pass its pending flag:

"use client";

import { LoadingBar } from "@azmr/ui/loading/client";
import { useRouter } from "next/navigation";
import { useTransition } from "react";

export function InvoiceLink({ id }: { id: string }) {
  const router = useRouter();
  const [pending, startTransition] = useTransition();
  return (
    <>
      <LoadingBar loading={pending} />
      <button type="button" onClick={() => startTransition(() => router.push(`/invoices/${id}`))}>
        Open
      </button>
    </>
  );
}

The fill moves with transform: scaleX, never width.

Reduced motion

A spinner, a ring around a logo and a pulsing logo keep moving under prefers-reduced-motion (data-motion="essential"): a frozen loader reads as a hung page. Fades and the bar's movement follow the user's setting and become instant; the delay still applies.

Props

Loading

Prop

Type

Spinner

Prop

Type

LoadingOverlay

Prop

Type

LoadingBar

Prop

Type

Accessibility

  • Loading is one role="status" region: its message is announced once, politely. With labelPlacement="hidden" the message is kept for screen readers only.
  • The spinner, the ring and the logo are aria-hidden; only the message is read.
  • LoadingOverlay keeps an empty status region in the page and fills it when the overlay appears, so the message is announced when it shows, and sets aria-busy on the content while it is stale.
  • LoadingBar is a progressbar named "Loading" by default (aria-label). It has no aria-valuenow while creeping, because that number is not real; with a known value it reports it.
  • Nothing here is focusable or takes keyboard input.