{"slug":"connectivity-error","category":"errors","job":"recover","added":"2026-09-14T10:42Z","title":"Connectivity Error","description":"A dismissible card surfacing a network connection failure with a description and retry/details actions.","url":"https://ai-patterns.app/patterns/errors/connectivity-error","markdown":"https://ai-patterns.app/patterns/errors/connectivity-error.md","json":"https://ai-patterns.app/api/patterns/errors/connectivity-error","seenIn":[{"team":"Claude","source":"https://support.claude.com/en/articles/10366432-i-m-getting-an-api-connection-error-how-can-i-fix-it"}],"spec":"# Connectivity Error\n\n## Summary\nA dismissible banner that surfaces a network connectivity failure with a short description and two recovery actions — \"View details\" and \"Try again\". Spans the full width of its container as a single row, appears inline in the layout (not as a toast or modal), and exits with a fade when dismissed or retried.\n\n## When to use\n- When the AI product loses its connection to the backend mid-session or on page load, and the user needs to know before submitting another prompt.\n- When the error may be caused by VPN, proxy, or local network configuration — not just a transient blip — and the user might need to investigate before retrying.\n- When both a passive \"View details\" path and an immediate \"Try again\" action are meaningful.\n\n## When not to use\n- Brief transient failures that auto-recover within seconds — surface a spinner instead and only show this if recovery fails after a threshold.\n- Hard authentication or quota failures — use a more specific error pattern (Rate Limit, account error) rather than a generic connectivity card.\n- When there is no meaningful \"View details\" target — omit that action rather than linking to a dead end.\n\n## Anatomy\n- **Container**: Single row, full width, small border radius, no shadow — reads as a slim banner rather than a card.\n- **Icon**: `WifiOff` (or similar offline icon) at 14px, next to the title. Decorative — `aria-hidden`.\n- **Title**: Short, factual label (\"Connection lost\"). Small, semibold, text-foreground.\n- **Description**: One short sentence, inline after the title on the same row. Small, muted text, truncates if the row runs out of space.\n- **View details button**: Small secondary bordered button in the trailing action group. Omitted if `onViewDetails` is not provided.\n- **Try again button**: Small secondary bordered button in the trailing action group. Omitted if `onRetry` is not provided.\n- **Close button**: Icon-only × button at the far right of the action group, hidden if `onClose` is not provided.\n\n## Behavior\n- Mounts with a short upward fade (opacity 0→1, y −6→0, 200 ms ease-out).\n- Exits with the reverse animation when the close button is clicked.\n- Pressing \"Try again\" should trigger the parent's retry logic; the demo resets the key so the card reappears after it has been dismissed.\n- Internal `visible` state gates the `AnimatePresence` exit — callers control re-showing by re-mounting (key reset) or by not providing `onClose`.\n- The card does not auto-dismiss or countdown — the user must act.\n\n## Content guidelines\n- Title: plain noun phrase describing the state, not an error code. \"Connection lost\" not \"ERR_NETWORK_CHANGED\" or \"Request failed (503)\".\n- Description: one sentence, action-oriented. Lead with what to check, not what went wrong. \"Check your internet connection, VPN or proxy and try again.\"\n- \"Try again\" is always the primary recovery; \"View details\" is optional and should link to a diagnostic panel or log, not an alert.\n- Keep both button labels under 20 characters — they share a single row.\n\n## Accessibility\n- The container has `role=\"alert\"` and `aria-live=\"assertive\"` so screen readers announce it immediately on mount.\n- The close button has `aria-label=\"Close\"`.\n- The wifi-off icon is `aria-hidden` — the title carries the semantic meaning.\n\n## Related patterns\n- **Rate Limit** — Use it instead when the request failed because of a quota, not the network.\n- **Partial Response** — Use it instead when the server cut the response short rather than the connection dropping.\n- **Console Error Card** — Use it instead when the error is for developers in the browser console, not end users.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { WifiOff, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ConnectivityErrorProps {\n  title?: string;\n  description?: string;\n  detailsLabel?: string;\n  retryLabel?: string;\n  onViewDetails?: () => void;\n  onRetry?: () => void;\n  onClose?: () => void;\n  className?: string;\n}\n\nexport function ConnectivityError({\n  title = \"Connection lost\",\n  description = \"Check your internet connection, VPN or proxy and try again.\",\n  detailsLabel = \"View details\",\n  retryLabel = \"Try again\",\n  onViewDetails,\n  onRetry,\n  onClose,\n  className,\n}: ConnectivityErrorProps) {\n  const [visible, setVisible] = React.useState(true);\n\n  function handleClose() {\n    setVisible(false);\n    onClose?.();\n  }\n\n  return (\n    <AnimatePresence>\n      {visible && (\n        <motion.div\n          initial={{ opacity: 0, y: -6 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: -6 }}\n          transition={{ duration: 0.2, ease: \"easeOut\" }}\n          role=\"alert\"\n          aria-live=\"assertive\"\n          className={cn(\n            \"flex w-full flex-wrap items-center gap-x-4 gap-y-2 overflow-hidden rounded-lg border border-border bg-background px-4 py-2.5 text-foreground\",\n            className\n          )}\n        >\n          {/* Message */}\n          <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n            <WifiOff className=\"size-3.5 shrink-0 text-foreground\" aria-hidden />\n            <span className=\"truncate text-xs font-semibold text-foreground\">{title}</span>\n            <span className=\"truncate text-xs text-muted-foreground\">{description}</span>\n          </div>\n\n          {/* Actions */}\n          <div className=\"flex shrink-0 items-center gap-2\">\n            {onViewDetails && (\n              <button\n                type=\"button\"\n                onClick={onViewDetails}\n                className=\"rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-foreground transition-colors hover:bg-accent\"\n              >\n                {detailsLabel}\n              </button>\n            )}\n            {onRetry && (\n              <button\n                type=\"button\"\n                onClick={onRetry}\n                className=\"rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-foreground transition-colors hover:bg-accent\"\n              >\n                {retryLabel}\n              </button>\n            )}\n            {onClose && (\n              <button\n                type=\"button\"\n                onClick={handleClose}\n                aria-label=\"Close\"\n                className=\"rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n              >\n                <X className=\"size-3.5\" aria-hidden />\n              </button>\n            )}\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}