Feedback

Spinner

An indeterminate activity indicator for waits with no known length. Takes the colour of its text.

Built on
<svg>role=statusprefers-reduced-motion
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.1 kB
Dependencies
React
Deploying to eu-west-1
Try its options

Loading its options.

Code
import { Spinner } from "@/components/equip/spinner";

export default function Demo() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--eq-ink-2)" }}>
      <Spinner size="s" label="Deploying to eu-west-1" />
      <span aria-hidden="true">Deploying to eu-west-1</span>
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--eq-ink-2)">
  <span role="status" class="eq-spinner" data-size="s">
    <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
      <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
      <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
    </svg>
    <span class="eq-spinner-label">Deploying to eu-west-1</span>
  </span>
  <span aria-hidden="true">Deploying to eu-west-1</span>
</div>

Examples

Sizes

Loading commentsLoading commentsLoading comments
Code
import { Spinner } from "@/components/equip/spinner";

export default function Example() {
  return (
    <div style={{ display: "flex", gap: 16, alignItems: "center" }}>
      <Spinner size="s" label="Loading comments" />
      <Spinner size="m" label="Loading comments" />
      <Spinner size="l" label="Loading comments" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:flex;gap:16px;align-items:center">
  <span role="status" class="eq-spinner" data-size="s">
    <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
      <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
      <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
    </svg>
    <span class="eq-spinner-label">Loading comments</span>
  </span>
  <span role="status" class="eq-spinner" data-size="m">
    <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
      <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
      <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
    </svg>
    <span class="eq-spinner-label">Loading comments</span>
  </span>
  <span role="status" class="eq-spinner" data-size="l">
    <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
      <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
      <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
    </svg>
    <span class="eq-spinner-label">Loading comments</span>
  </span>
</div>

Takes the text colour

The spinner draws in currentColor, so it follows whatever text it sits beside without a tone prop.

Syncing contactsChecking for updatesRetrying payment webhook
Code
import { Spinner } from "@/components/equip/spinner";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 10, fontSize: 13 }}>
      <span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-ink)" }}>
        <Spinner size="s" label="Syncing contacts" />
        <span aria-hidden="true">Syncing contacts</span>
      </span>
      <span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-ink-3)" }}>
        <Spinner size="s" label="Checking for updates" />
        <span aria-hidden="true">Checking for updates</span>
      </span>
      <span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-warn)" }}>
        <Spinner size="s" label="Retrying payment webhook" />
        <span aria-hidden="true">Retrying payment webhook</span>
      </span>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:10px;font-size:13px">
  <span style="display:flex;align-items:center;gap:8px;color:var(--eq-ink)">
    <span role="status" class="eq-spinner" data-size="s">
      <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
        <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
        <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
      </svg>
      <span class="eq-spinner-label">Syncing contacts</span>
    </span>
    <span aria-hidden="true">Syncing contacts</span>
  </span>
  <span style="display:flex;align-items:center;gap:8px;color:var(--eq-ink-3)">
    <span role="status" class="eq-spinner" data-size="s">
      <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
        <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
        <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
      </svg>
      <span class="eq-spinner-label">Checking for updates</span>
    </span>
    <span aria-hidden="true">Checking for updates</span>
  </span>
  <span style="display:flex;align-items:center;gap:8px;color:var(--eq-warn)">
    <span role="status" class="eq-spinner" data-size="s">
      <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
        <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
        <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
      </svg>
      <span class="eq-spinner-label">Retrying payment webhook</span>
    </span>
    <span aria-hidden="true">Retrying payment webhook</span>
  </span>
</div>

Holding a panel

The panel sets aria-busy while it waits, and the spinner's label says what it is waiting for.

Loading build logs
Code
import { Spinner } from "@/components/equip/spinner";

export default function Example() {
  return (
    <section
      aria-busy="true"
      aria-label="Build logs"
      style={{
        display: "grid",
        placeItems: "center",
        width: "100%",
        maxWidth: 280,
        height: 96,
        border: "1px solid var(--eq-line)",
        borderRadius: "var(--eq-radius-l)",
        color: "var(--eq-ink-3)",
      }}
    >
      <Spinner size="l" label="Loading build logs" />
    </section>
  );
}
HTMLworks in any framework, with the part's CSS
<section aria-busy="true" aria-label="Build logs" style="display:grid;place-items:center;width:100%;max-width:280px;height:96px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l);color:var(--eq-ink-3)">
  <span role="status" class="eq-spinner" data-size="l">
    <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
      <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
      <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
    </svg>
    <span class="eq-spinner-label">Loading build logs</span>
  </span>
</section>

Props

PropTypeDefaultWhat it does
size"s" | "m" | "l""m"16, 20 or 24px.
labelstring"Loading"Read by screen readers and visually hidden. Say what is loading.
...restComponentProps<"span">noneEvery native span attribute.

Anatomy

root.eq-spinnerA span with role="status".
ring.eq-spinner-ringThe SVG. Hidden from assistive tech.
track.eq-spinner-trackThe faint full circle behind the arc.
arc.eq-spinner-arc
label.eq-spinner-labelVisually hidden text.

Notes

  • The root is role="status", a polite live region; a status that is already on the page when its text appears is announced more reliably than one inserted with its text.
  • Make the label specific, such as "Loading invoices", and remove the spinner when the content arrives.
  • Colour comes from currentColor, so it matches the text around it and needs no tone prop.
  • Under prefers-reduced-motion the turn slows to 2.4 seconds instead of stopping, so it still reads as working.
  • For waits with a known length, show progress instead.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/spinner.tsx34 lines
import type { ComponentProps } from "react";
import "./spinner.css";

export type SpinnerProps = ComponentProps<"span"> & {
  /** 16, 20 or 24px. */
  size?: "s" | "m" | "l";
  /** What is happening, read by screen readers. Make it specific: "Loading invoices". */
  label?: string;
};

export function Spinner({ size = "m", label = "Loading", className, ...rest }: SpinnerProps) {
  return (
    <span
      role="status"
      {...rest}
      className={["eq-spinner", className].filter(Boolean).join(" ")}
      data-size={size}
    >
      <svg
        className="eq-spinner-ring"
        viewBox="0 0 16 16"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        aria-hidden="true"
      >
        <circle className="eq-spinner-track" cx="8" cy="8" r="6.25" />
        <circle className="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" strokeDasharray="28 100" />
      </svg>
      <span className="eq-spinner-label">{label}</span>
    </span>
  );
}
components/equip/spinner.css63 lines
@layer equip {
  .eq-spinner {
    --_size: 20px;

    position: relative;
    display: inline-flex;
    flex: none;
    width: var(--_size);
    height: var(--_size);
    vertical-align: middle;
  }

  .eq-spinner[data-size="s"] {
    --_size: 16px;
  }

  .eq-spinner[data-size="l"] {
    --_size: 24px;
  }

  .eq-spinner-ring {
    display: block;
    width: 100%;
    height: 100%;
    /* Start the arc at twelve o'clock. */
    rotate: -90deg;
    animation: eq-spinner-spin 700ms linear infinite;
  }

  .eq-spinner-track {
    opacity: 0.2;
  }

  @keyframes eq-spinner-spin {
    from {
      rotate: -90deg;
    }

    to {
      rotate: 270deg;
    }
  }

  /* The one motion allowed to run on its own. It slows down rather than stopping,
     because a still arc would read as finished. A literal duration, since --eq-dur is 0 here. */
  @media (prefers-reduced-motion: reduce) {
    .eq-spinner-ring {
      animation-duration: 2.4s;
    }
  }

  .eq-spinner-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index