macbetter

Status

Activity Indicator

Show that something is happening when there's nothing to measure.

import { ActivityIndicator } from "macbetter/activity-indicator"

export default function Example() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 32, color: "var(--mb-label-secondary)" }}>
      <ActivityIndicator size="sm" />
      <ActivityIndicator />
      <ActivityIndicator size="lg" />
    </div>
  )
}

When to use it

Use an activity indicator for a short wait whose length you can't tell: loading a list, signing in, fetching a preview. If you know how far along the work is, use a progress bar instead, because a bar tells people how long to wait.

Keep it near what's loading, and say what's happening beside it when there's room. Remove it as soon as the content arrives, and don't show one for waits under about a second; a flash of a spinner reads as a glitch.

Usage

import { ActivityIndicator } from "macbetter/activity-indicator"

It draws in currentColor, so it matches the text around it. Apple's is gray: put it in secondary label color, as in these demos.

With a label

When text beside it already says what's loading, give the indicator the same words as its aria-label and hide the text from screen readers, so it's read once.

import { ActivityIndicator } from "macbetter/activity-indicator"

export default function Example() {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: 8,
        color: "var(--mb-label-secondary)",
      }}
    >
      <ActivityIndicator aria-label="Loading messages" />
      <span aria-hidden="true">Loading Messages…</span>
    </div>
  )
}

Color

On a dark overlay, or in the accent, set the color on a parent.

import { ActivityIndicator } from "macbetter/activity-indicator"

export default function Example() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 24 }}>
      <span style={{ color: "var(--mb-label)" }}>
        <ActivityIndicator size="lg" aria-label="Preparing export" />
      </span>
      <span style={{ color: "var(--mb-accent-text)" }}>
        <ActivityIndicator size="lg" aria-label="Syncing" />
      </span>
      <span
        style={{
          display: "inline-grid",
          placeItems: "center",
          width: 88,
          height: 88,
          borderRadius: 20,
          background: "rgb(0 0 0 / 0.72)",
          color: "#ffffff",
        }}
      >
        <ActivityIndicator size="lg" aria-label="Processing video" />
      </span>
    </div>
  )
}

Platform behavior

The sizes follow each platform's native ones. On macOS, sm, md and lg are 12, 16 and 32 points, the mini, small and regular spinning indicators. On iOS and iPadOS they're 16, 20 and 37 points, UIActivityIndicatorView's medium and large.

The spokes are capsules, as on iOS 13 and later and macOS 11 and later. Each one lights fully and then fades until its turn comes round again, so the bright spoke travels clockwise with a fading tail. It keeps turning with Reduce Motion on, as the system spinner does: the motion is the message.

Accessibility

  • It's an indeterminate progressbar, named "Loading" unless you give it an aria-label. Name it for what's loading when that helps: "Loading messages".
  • It never takes focus.
  • If the loading region should be announced as busy, set aria-busy on that region.

API

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"The platform's mini, small and regular sizes.
aria-labelstring"Loading"What's loading.

It takes ui-sdk Progress props other than value.

Attribute or variableOnDescription
data-indeterminate.mb-activity-indicatorAlways present, from ui-sdk.
data-size.mb-activity-indicatorThe size.
--mb-activity-indicator-duration.mb-activity-indicatorHow long one turn takes. 0.8s by default.
--mb-activity-indicator-size-sm, --mb-activity-indicator-size, --mb-activity-indicator-size-lgany ancestorThe three sizes.