Skip to content

ConnectionStatus

MD · 1950B gzip budget

A live banner for offline, restored, and constrained connections.

Category
Feedback & status
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/connection-status.json

Copies the source into your project. Pulls in 4 shared registry items: cn, use-network, use-locale, locale.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useEffect, useRef, useState } from "react";
import { cn } from "../lib/cn";
import { useLocale } from "../lib/use-locale";
import { useNetwork } from "../lib/use-network";

export interface ConnectionStatusLabels {
  offline: string;
  /** Shown briefly after the connection comes back. */
  restored: string;
  /** Shown on 2G-class or Save-Data connections. */
  slow: string;
}

const DEFAULT_LABELS: ConnectionStatusLabels = {
  offline: "You're offline. Changes are saved on this device.",
  restored: "Back online.",
  slow: "Slow connection. Loading a lighter version.",
};

export interface ConnectionStatusProps {
  labels?: Partial<ConnectionStatusLabels>;
  /** How long the "back online" confirmation stays up, in ms. */
  restoredDuration?: number;
  /** Say nothing about slow-but-working connections. */
  hideSlow?: boolean;
  className?: string;
}

/**
 * A live banner for the state of the user's connection.
 *
 * Apps built on fast office Wi-Fi tend to fail silently on a train: the request
 * hangs, nothing changes on screen, and the user taps the button again. This
 * says what is happening, in the page's own language and direction, and says it
 * out loud for screen readers — politely, because losing signal is not an
 * emergency that should interrupt whatever is being read.
 */
export function ConnectionStatus({
  labels: labelOverrides,
  restoredDuration = 4000,
  hideSlow = false,
  className,
}: ConnectionStatusProps) {
  const labels = { ...DEFAULT_LABELS, ...labelOverrides };
  const { online, constrained } = useNetwork();
  const { direction } = useLocale();

  const [showRestored, setShowRestored] = useState(false);
  const wasOffline = useRef(false);

  useEffect(() => {
    if (!online) {
      // No state update needed: while offline the offline message wins in
      // render regardless of what `showRestored` happens to hold.
      wasOffline.current = true;
      return;
    }

    if (!wasOffline.current) return;

    wasOffline.current = false;
    setShowRestored(true);

    const timer = setTimeout(() => setShowRestored(false), restoredDuration);
    return () => clearTimeout(timer);
  }, [online, restoredDuration]);

  const message = !online
    ? labels.offline
    : showRestored
      ? labels.restored
      : !hideSlow && constrained
        ? labels.slow
        : null;

  const tone = !online ? "offline" : showRestored ? "restored" : "slow";

  return (
    // The region is always mounted, even when empty: a live region created at
    // the same moment its text appears is not announced by most screen readers.
    <div
      role="status"
      aria-live="polite"
      dir={direction}
      className={cn(
        "text-start text-sm transition-[opacity,padding] duration-200 motion-reduce:transition-none",
        message ? "px-4 py-2 opacity-100" : "h-0 overflow-hidden p-0 opacity-0",
        message &&
          {
            offline:
              "bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900",
            restored:
              "bg-green-100 text-green-900 dark:bg-green-950 dark:text-green-100",
            slow: "bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-100",
          }[tone],
        className,
      )}
    >
      {message}
    </div>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation