# Status & Error Pages

> 404, 500, maintenance and offline screens with a segmented preview switcher, search, retry and subscribe interactions.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/status-error-pages
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/status-error-pages.json
```

Adds the page at `app/status-error-pages/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [Newsletter Signup](https://chromotion.dev/r/newsletter-signup.md): `@/components/newsletter-signup`
- [Segmented Control](https://chromotion.dev/r/segmented-control.md): `@/components/segmented-control`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/status-error-pages/page.tsx"
"use client"

import * as React from "react"
import {
  Activity,
  ArrowRight,
  BookOpen,
  Check,
  CircleCheck,
  Compass,
  CreditCard,
  Hexagon,
  LayoutDashboard,
  LifeBuoy,
  LoaderCircle,
  RefreshCw,
  Search,
  ServerCrash,
  Sparkles,
  Wifi,
  WifiOff,
  Wrench,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { NewsletterSignup } from "@/components/newsletter-signup"
import { SegmentedControl } from "@/components/segmented-control"

type StateId = "404" | "500" | "maintenance" | "offline"

const tones = {
  indigo: { badge: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400", code: "from-indigo-500 to-fuchsia-500", glow: "bg-indigo-500/20" },
  rose: { badge: "bg-rose-500/10 text-rose-600 dark:text-rose-400", code: "from-rose-500 to-orange-500", glow: "bg-rose-500/20" },
  amber: { badge: "bg-amber-500/10 text-amber-600 dark:text-amber-400", code: "from-amber-500 to-orange-500", glow: "bg-amber-500/20" },
  sky: { badge: "bg-sky-500/10 text-sky-600 dark:text-sky-400", code: "from-sky-500 to-indigo-500", glow: "bg-sky-500/20" },
}

const primaryButton =
  "inline-flex h-11 items-center justify-center gap-2 rounded-xl bg-primary px-5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none disabled:pointer-events-none disabled:opacity-70"
const secondaryButton =
  "inline-flex h-11 items-center justify-center gap-2 rounded-xl border bg-background px-5 text-sm font-medium shadow-xs transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"

function StateShell({
  tone,
  icon: Icon,
  code,
  title,
  description,
  children,
}: {
  tone: keyof typeof tones
  icon: React.ComponentType<{ className?: string }>
  code?: string
  title: string
  description: React.ReactNode
  children: React.ReactNode
}) {
  const style = tones[tone]
  return (
    <section aria-labelledby="state-title" className="relative mx-auto flex w-full max-w-xl flex-col items-center gap-5 text-center">
      <div aria-hidden className={cn("pointer-events-none absolute -top-10 left-1/2 -z-10 size-72 -translate-x-1/2 rounded-full blur-3xl", style.glow)} />
      <span aria-hidden className={cn("flex size-14 items-center justify-center rounded-2xl", style.badge)}>
        <Icon className="size-7" />
      </span>
      {code && (
        <p aria-hidden className={cn("bg-linear-to-br bg-clip-text text-8xl leading-none font-bold tracking-tighter text-transparent sm:text-9xl", style.code)}>
          {code}
        </p>
      )}
      <h1 id="state-title" className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
        {code && <span className="sr-only">Error {code}: </span>}
        {title}
      </h1>
      <p className="max-w-md text-pretty text-muted-foreground">{description}</p>
      {children}
    </section>
  )
}

/** Simulates an action that fails until the given attempt succeeds. */
function useSimulatedRetry(successOnAttempt: number) {
  const [attempts, setAttempts] = React.useState(0)
  const [busy, setBusy] = React.useState(false)

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setAttempts((count) => count + 1)
    }, 1600)
    return () => clearTimeout(timer)
  }, [busy])

  return {
    attempts,
    busy,
    recovered: attempts >= successOnAttempt,
    start: () => setBusy(true),
    reset: () => setAttempts(0),
  }
}

function Spinner() {
  return <LoaderCircle aria-hidden className="size-4 animate-spin motion-reduce:animate-none" />
}

const suggestions = [
  { label: "Dashboard", description: "Your projects and deployments", icon: LayoutDashboard },
  { label: "Documentation", description: "Guides, API reference and examples", icon: BookOpen },
  { label: "Pricing", description: "Plans for teams of every size", icon: CreditCard },
  { label: "Changelog", description: "What shipped this month", icon: Sparkles },
  { label: "System status", description: "Live uptime and incident history", icon: Activity },
  { label: "Contact support", description: "Talk to a real person", icon: LifeBuoy },
]

function NotFound() {
  const searchId = React.useId()
  const [query, setQuery] = React.useState("")
  const needle = query.trim().toLowerCase()
  const results = suggestions.filter((item) => !needle || `${item.label} ${item.description}`.toLowerCase().includes(needle))

  return (
    <StateShell
      tone="indigo"
      icon={Compass}
      code="404"
      title="Page not found"
      description="The page you are looking for has moved, been renamed or never existed. Try searching, or jump to one of the pages below."
    >
      <form role="search" onSubmit={(event) => event.preventDefault()} className="flex w-full max-w-md gap-2">
        <div className="relative min-w-0 flex-1">
          <label htmlFor={searchId} className="sr-only">
            Search the site
          </label>
          <Search aria-hidden className="pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-muted-foreground" />
          <input
            id={searchId}
            type="search"
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search pages, docs and guides"
            autoComplete="off"
            className="h-11 w-full rounded-xl border bg-background pr-3 pl-10 text-sm shadow-xs outline-none transition-[border-color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20"
          />
        </div>
        <button type="submit" className={cn(primaryButton, "px-4")}>
          Search
        </button>
      </form>

      <div className="w-full max-w-md text-left">
        <h2 className="mb-2 px-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">{needle ? "Matching pages" : "Popular pages"}</h2>
        <p role="status" className="sr-only">
          {results.length} {results.length === 1 ? "page" : "pages"} found
        </p>
        {results.length === 0 ? (
          <div className="rounded-xl border border-dashed p-6 text-center text-sm">
            <p className="font-medium">No pages match “{query.trim()}”</p>
            <p className="mt-1 text-muted-foreground">Check the spelling, or ask our support team for help.</p>
          </div>
        ) : (
          <ul className="divide-y overflow-hidden rounded-xl border bg-card">
            {results.map(({ label, description, icon: Icon }) => (
              <li key={label}>
                <a href="#" className="group flex items-center gap-3 px-4 py-3 transition-colors hover:bg-accent focus-visible:bg-accent focus-visible:outline-none">
                  <span aria-hidden className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
                    <Icon className="size-4" />
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-sm font-medium">{label}</span>
                    <span className="block truncate text-xs text-muted-foreground">{description}</span>
                  </span>
                  <ArrowRight aria-hidden className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 motion-reduce:transition-none" />
                </a>
              </li>
            ))}
          </ul>
        )}
      </div>

      <a href="#" className={primaryButton}>
        Back to home
      </a>
    </StateShell>
  )
}

function ServerError() {
  const retry = useSimulatedRetry(2)

  return (
    <StateShell
      tone="rose"
      icon={ServerCrash}
      code="500"
      title="Something went wrong on our end"
      description="An unexpected error stopped this page from loading. Our engineers have been notified and are already looking into it."
    >
      <div className="flex flex-col items-center gap-3">
        <div className="flex flex-wrap justify-center gap-3">
          {retry.recovered ? (
            <button type="button" onClick={retry.reset} className={primaryButton}>
              <Check className="size-4" />
              Reload page
            </button>
          ) : (
            <button type="button" onClick={retry.start} disabled={retry.busy} aria-busy={retry.busy} className={primaryButton}>
              {retry.busy ? <Spinner /> : <RefreshCw className="size-4" />}
              {retry.busy ? "Retrying…" : "Try again"}
            </button>
          )}
          <a href="#" className={secondaryButton}>
            Go to status page
          </a>
        </div>
        <p role="status" className={cn("min-h-5 text-sm", retry.recovered ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground")}>
          {retry.busy
            ? "Contacting the server…"
            : retry.recovered
              ? "The server is responding again. Reload to continue."
              : retry.attempts > 0
                ? `Still failing after ${retry.attempts} ${retry.attempts === 1 ? "attempt" : "attempts"}. Please try once more.`
                : ""}
        </p>
        <p className="rounded-lg bg-muted px-3 py-1.5 font-mono text-xs text-muted-foreground">Reference: HX-5F3A91</p>
      </div>
    </StateShell>
  )
}

const maintenanceSteps = [
  { label: "Database upgrade", state: "done" },
  { label: "Cache warm-up", state: "current" },
  { label: "Final health checks", state: "todo" },
] as const

function Maintenance() {
  return (
    <StateShell
      tone="amber"
      icon={Wrench}
      title="We’ll be right back"
      description="Hexa is down for scheduled maintenance while we upgrade our infrastructure. Your data is safe and nothing needs to be done on your side."
    >
      <div className="w-full max-w-md rounded-2xl border bg-card p-5 text-left shadow-xs">
        <div className="flex items-baseline justify-between gap-3 text-sm">
          <p className="font-medium">Maintenance progress</p>
          <p className="tabular-nums text-muted-foreground">72%</p>
        </div>
        <div
          role="progressbar"
          aria-label="Maintenance progress"
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={72}
          aria-valuetext="72% complete"
          className="mt-3 h-2.5 overflow-hidden rounded-full bg-muted"
        >
          <div className="h-full w-[72%] rounded-full bg-linear-to-r from-amber-400 to-orange-500 motion-safe:animate-pulse" />
        </div>
        <p className="mt-3 text-sm text-muted-foreground">
          Estimated completion: <span className="font-medium text-foreground">14:30 UTC</span> (about 25 minutes)
        </p>
        <ul className="mt-4 flex flex-col gap-2 border-t pt-4 text-sm">
          {maintenanceSteps.map((step) => (
            <li key={step.label} className="flex items-center gap-2.5">
              <span
                aria-hidden
                className={cn(
                  "flex size-5 shrink-0 items-center justify-center rounded-full border",
                  step.state === "done" && "border-emerald-500 bg-emerald-500 text-white",
                  step.state === "current" && "border-amber-500"
                )}
              >
                {step.state === "done" ? <Check className="size-3" /> : step.state === "current" ? <span className="size-2 rounded-full bg-amber-500 motion-safe:animate-pulse" /> : null}
              </span>
              <span className={cn(step.state === "todo" && "text-muted-foreground")}>
                {step.label}
                <span className="sr-only">{step.state === "done" ? " (complete)" : step.state === "current" ? " (in progress)" : " (pending)"}</span>
              </span>
            </li>
          ))}
        </ul>
      </div>

      <div className="flex w-full max-w-md flex-col gap-2 text-left">
        <p className="px-1 text-sm font-medium">Get notified when we are back</p>
        <NewsletterSignup
          buttonLabel="Notify me"
          placeholder="you@company.com"
          hint="One email, as soon as the service is restored."
          successMessage="Done. We’ll email you the moment Hexa is back online."
          onSubscribe={() => new Promise<void>((resolve) => setTimeout(resolve, 700))}
        />
      </div>
    </StateShell>
  )
}

function Offline() {
  const reconnect = useSimulatedRetry(2)

  return (
    <StateShell
      tone="sky"
      icon={reconnect.recovered ? Wifi : WifiOff}
      title={reconnect.recovered ? "You’re back online" : "You’re offline"}
      description={
        reconnect.recovered
          ? "Your connection is working again. Any changes you made while offline have been synced."
          : "We can’t reach the network right now. Check your connection and try again. Your work is saved on this device."
      }
    >
      <div className="flex flex-col items-center gap-3">
        {reconnect.recovered ? (
          <button type="button" onClick={reconnect.reset} className={primaryButton}>
            <CircleCheck className="size-4" />
            Continue
          </button>
        ) : (
          <button type="button" onClick={reconnect.start} disabled={reconnect.busy} aria-busy={reconnect.busy} className={primaryButton}>
            {reconnect.busy ? <Spinner /> : <RefreshCw className="size-4" />}
            {reconnect.busy ? "Reconnecting…" : "Try to reconnect"}
          </button>
        )}
        <p role="status" className={cn("min-h-5 text-sm", reconnect.recovered ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground")}>
          {reconnect.busy
            ? "Checking your connection…"
            : reconnect.recovered
              ? "Connected. All changes are synced."
              : reconnect.attempts > 0
                ? "Still offline. We couldn’t reach the network."
                : ""}
        </p>
      </div>
      {!reconnect.recovered && (
        <ul className="w-full max-w-md divide-y rounded-2xl border bg-card text-left text-sm">
          {["Check that Wi-Fi or mobile data is turned on", "Move closer to your router or switch networks", "Turn off airplane mode or any active VPN"].map((tip) => (
            <li key={tip} className="flex items-center gap-3 px-4 py-3">
              <span aria-hidden className="size-1.5 shrink-0 rounded-full bg-muted-foreground/50" />
              {tip}
            </li>
          ))}
        </ul>
      )}
    </StateShell>
  )
}

export default function StatusErrorPages() {
  const [state, setState] = React.useState<StateId>("404")

  return (
    <div className="flex min-h-svh flex-col bg-background text-foreground">
      <header className="border-b">
        <div className="mx-auto flex max-w-5xl flex-wrap items-center justify-between gap-x-6 gap-y-3 px-4 py-3 sm:px-6">
          <a href="#" className="flex items-center gap-2 font-semibold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
            <Hexagon className="size-5 fill-primary text-primary" /> Hexa
          </a>
          <div className="flex max-w-full items-center gap-3">
            <span className="hidden text-sm text-muted-foreground sm:inline">Preview state</span>
            <div className="-m-1 max-w-full overflow-x-auto p-1">
              <SegmentedControl
                size="sm"
                aria-label="Preview state"
                value={state}
                onValueChange={(value) => setState(value as StateId)}
                options={[
                  { value: "404", label: "404" },
                  { value: "500", label: "500" },
                  { value: "maintenance", label: "Maintenance" },
                  { value: "offline", label: "Offline" },
                ]}
              />
            </div>
          </div>
        </div>
      </header>

      <main className="flex flex-1 items-center justify-center px-4 py-12 sm:px-6 sm:py-16">
        {state === "404" && <NotFound />}
        {state === "500" && <ServerError />}
        {state === "maintenance" && <Maintenance />}
        {state === "offline" && <Offline />}
      </main>

      <footer className="border-t px-4 py-5 text-center text-xs text-muted-foreground sm:px-6">
        © 2026 Hexa Labs, Inc. · <a href="#" className="underline-offset-4 hover:text-foreground hover:underline">Status</a> ·{" "}
        <a href="#" className="underline-offset-4 hover:text-foreground hover:underline">Support</a>
      </footer>
    </div>
  )
}
```
