Web InnoventixFreeCode

Error Under Construction

Original · free

under construction page

byWeb InnoventixReact + Tailwind
errorunderconstructionpages
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/error-under-construction.json
error-under-construction.tsx
"use client";

import { useCallback, useId, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";

type PhaseStatus = "complete" | "active" | "queued";

interface Phase {
  id: string;
  label: string;
  status: PhaseStatus;
  percent: number;
  headline: string;
  note: string;
  items: readonly string[];
}

const PHASES: readonly Phase[] = [
  {
    id: "foundations",
    label: "Foundations",
    status: "complete",
    percent: 100,
    headline: "Signed off 14 July",
    note: "Routing, the component data model and the auth shell all landed. 24 of 24 checks green on the last run.",
    items: [
      "URL scheme locked to /playground/[slug]",
      "Registry schema migrated, 831 entries validated",
      "Session + rate limiting behind the preview route",
    ],
  },
  {
    id: "framing",
    label: "Framing",
    status: "active",
    percent: 64,
    headline: "In progress — 9 of 14 tickets merged",
    note: "The layout shell, prop controls and live preview pane are going up now. This is the loud part of the build.",
    items: [
      "Split-pane preview with resizable canvas — merged",
      "Prop controls for booleans, enums and strings — merged",
      "Theme + viewport switcher — in review",
      "Error boundary around user-edited props — open",
    ],
  },
  {
    id: "fit-out",
    label: "Fit-out",
    status: "queued",
    percent: 0,
    headline: "Starts once framing is signed off",
    note: "Docs, keyboard shortcuts, shareable snapshot links and the copy-to-clipboard flow. Nothing here is started yet.",
    items: [
      "Per-component usage docs pulled from source",
      "Cmd+K palette and shortcut sheet",
      "Share a playground state as a short link",
    ],
  },
];

const OVERALL_PERCENT = 62;

function HardHatIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
      focusable="false"
    >
      <path d="M4 16a8 8 0 0 1 16 0" />
      <path d="M10 16V6.5A1.5 1.5 0 0 1 11.5 5h1A1.5 1.5 0 0 1 14 6.5V16" />
      <path d="M2.5 16h19" />
      <path d="M3.5 19h17" />
    </svg>
  );
}

function CheckIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.4"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-3 w-3"
      aria-hidden="true"
      focusable="false"
    >
      <path d="m4 12.5 5 5L20 6.5" />
    </svg>
  );
}

function WrenchIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-3 w-3"
      aria-hidden="true"
      focusable="false"
    >
      <path d="M15.5 3.5a5 5 0 0 0-6.2 6.2L3.7 15.3a2 2 0 0 0 0 2.8l2.2 2.2a2 2 0 0 0 2.8 0l5.6-5.6a5 5 0 0 0 6.2-6.2l-3.1 3.1-2.8-.4-.4-2.8Z" />
    </svg>
  );
}

function ClockIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-3 w-3"
      aria-hidden="true"
      focusable="false"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7.5V12l3 2" />
    </svg>
  );
}

function ArrowLeftIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
      focusable="false"
    >
      <path d="M19 12H5" />
      <path d="m11 6-6 6 6 6" />
    </svg>
  );
}

function ArrowRightIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
      focusable="false"
    >
      <path d="M5 12h14" />
      <path d="m13 6 6 6-6 6" />
    </svg>
  );
}

function StatusChip({ status }: { status: PhaseStatus }) {
  if (status === "complete") {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-500/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-emerald-700 ring-1 ring-inset ring-emerald-600/25 dark:text-emerald-300 dark:ring-emerald-400/25">
        <CheckIcon />
        Complete
      </span>
    );
  }
  if (status === "active") {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full bg-amber-500/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-amber-700 ring-1 ring-inset ring-amber-600/30 dark:text-amber-300 dark:ring-amber-400/30">
        <WrenchIcon />
        Building now
      </span>
    );
  }
  return (
    <span className="inline-flex items-center gap-1.5 rounded-full bg-zinc-500/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-zinc-600 ring-1 ring-inset ring-zinc-500/25 dark:text-zinc-400 dark:ring-zinc-400/20">
      <ClockIcon />
      Queued
    </span>
  );
}

export default function ErrorUnderConstruction() {
  const reduceMotion = useReducedMotion();
  const baseId = useId();
  const [activeIndex, setActiveIndex] = useState<number>(1);
  const [blueprint, setBlueprint] = useState<boolean>(false);
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const activePhase = PHASES[activeIndex];

  const focusTab = useCallback((index: number) => {
    setActiveIndex(index);
    tabRefs.current[index]?.focus();
  }, []);

  const onTabKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>) => {
      const last = PHASES.length - 1;
      if (event.key === "ArrowRight" || event.key === "ArrowDown") {
        event.preventDefault();
        focusTab(activeIndex === last ? 0 : activeIndex + 1);
      } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
        event.preventDefault();
        focusTab(activeIndex === 0 ? last : activeIndex - 1);
      } else if (event.key === "Home") {
        event.preventDefault();
        focusTab(0);
      } else if (event.key === "End") {
        event.preventDefault();
        focusTab(last);
      }
    },
    [activeIndex, focusTab],
  );

  return (
    <section className="relative w-full overflow-hidden bg-white px-5 py-20 text-zinc-900 sm:px-8 sm:py-28 dark:bg-zinc-950 dark:text-zinc-50">
      <style>{`
        @keyframes euc-stripe-drift {
          from { background-position: 0 0; }
          to { background-position: 64px 0; }
        }
        @keyframes euc-sweep {
          0% { transform: translateY(-110%); opacity: 0; }
          22% { opacity: 1; }
          78% { opacity: 1; }
          100% { transform: translateY(560%); opacity: 0; }
        }
        @keyframes euc-beacon {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.35; transform: scale(0.72); }
        }
        .euc-stripes {
          background-image: repeating-linear-gradient(
            135deg,
            currentColor 0 12px,
            transparent 12px 32px
          );
          background-size: 64px 64px;
          animation: euc-stripe-drift 2.6s linear infinite;
        }
        .euc-grid {
          background-image:
            linear-gradient(to right, currentColor 1px, transparent 1px),
            linear-gradient(to bottom, currentColor 1px, transparent 1px);
          background-size: 28px 28px;
        }
        .euc-sweep {
          animation: euc-sweep 6.5s cubic-bezier(0.65, 0, 0.35, 1) infinite;
        }
        .euc-beacon {
          animation: euc-beacon 1.5s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .euc-stripes,
          .euc-sweep,
          .euc-beacon {
            animation: none !important;
          }
          .euc-sweep {
            opacity: 0;
          }
        }
      `}</style>

      <div
        aria-hidden="true"
        className={`pointer-events-none absolute inset-0 text-sky-500/[0.16] transition-opacity duration-500 dark:text-sky-300/[0.14] ${
          blueprint ? "euc-grid opacity-100" : "opacity-0"
        }`}
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-40 left-1/2 h-80 w-[42rem] -translate-x-1/2 rounded-full bg-amber-300/25 blur-3xl dark:bg-amber-500/10"
      />

      <div className="relative mx-auto max-w-6xl">
        <div
          aria-hidden="true"
          className="euc-stripes h-2.5 w-full rounded-full text-amber-400 dark:text-amber-300/80"
        />

        <div className="mt-12 grid grid-cols-1 gap-12 lg:mt-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.05fr)] lg:gap-16">
          <div>
            <p className="inline-flex items-center gap-2 rounded-full bg-amber-500/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-amber-800 ring-1 ring-inset ring-amber-600/25 dark:text-amber-200 dark:ring-amber-400/25">
              <HardHatIcon />
              Zone C · Crew on site
            </p>

            <h1 className="mt-6 text-4xl font-semibold leading-[1.08] tracking-tight text-zinc-900 sm:text-5xl lg:text-6xl dark:text-white">
              This page is still
              <span className="block text-amber-600 dark:text-amber-400">
                a building site.
              </span>
            </h1>

            <p className="mt-6 max-w-xl text-base leading-relaxed text-zinc-600 sm:text-lg dark:text-zinc-400">
              The component playground is going up right now. Foundations are
              poured and the framing is half up — but the doors aren&apos;t on
              yet, so we&apos;re keeping visitors behind the tape. Everything
              else on the site is open and running as normal.
            </p>

            <div className="mt-10 rounded-2xl border border-zinc-200 bg-zinc-50/70 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
              <div className="flex items-baseline justify-between gap-4">
                <span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
                  Overall build progress
                </span>
                <span className="text-2xl font-semibold tabular-nums text-zinc-900 dark:text-white">
                  {OVERALL_PERCENT}
                  <span className="text-base text-zinc-500 dark:text-zinc-500">
                    %
                  </span>
                </span>
              </div>
              <div
                role="progressbar"
                aria-valuenow={OVERALL_PERCENT}
                aria-valuemin={0}
                aria-valuemax={100}
                aria-label="Overall build progress"
                className="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-800"
              >
                <motion.div
                  className="h-full rounded-full bg-amber-500 dark:bg-amber-400"
                  initial={reduceMotion ? false : { width: 0 }}
                  animate={{ width: `${OVERALL_PERCENT}%` }}
                  transition={
                    reduceMotion
                      ? { duration: 0 }
                      : { duration: 1.1, ease: [0.22, 1, 0.36, 1], delay: 0.15 }
                  }
                  style={reduceMotion ? { width: `${OVERALL_PERCENT}%` } : undefined}
                />
              </div>
              <p className="mt-3 flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-500">
                <span className="euc-beacon inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500 dark:bg-amber-400" />
                Handover targeted for Friday 8 August · last commit 40 minutes
                ago
              </p>
            </div>

            <div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center">
              <a
                href="/"
                className="inline-flex items-center justify-center gap-2 rounded-xl bg-zinc-900 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200 dark:focus-visible:ring-offset-zinc-950"
              >
                <ArrowLeftIcon />
                Back to the homepage
              </a>
              <a
                href="/changelog"
                className="group inline-flex items-center justify-center gap-2 rounded-xl border border-zinc-300 px-5 py-3 text-sm font-semibold text-zinc-800 transition-colors hover:border-zinc-400 hover:bg-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:text-zinc-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-900 dark:focus-visible:ring-offset-zinc-950"
              >
                Read the build log
                <span className="transition-transform group-hover:translate-x-0.5">
                  <ArrowRightIcon />
                </span>
              </a>
            </div>
          </div>

          <div className="relative">
            <div className="relative overflow-hidden rounded-3xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-7 dark:border-zinc-800 dark:bg-zinc-900/70">
              <div
                aria-hidden="true"
                className="euc-sweep pointer-events-none absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-transparent via-sky-400/10 to-transparent dark:via-sky-300/10"
              />

              <div className="relative flex flex-wrap items-center justify-between gap-4">
                <div>
                  <h2 className="text-sm font-semibold text-zinc-900 dark:text-white">
                    Site plan
                  </h2>
                  <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-500">
                    Inspect any phase of the build
                  </p>
                </div>

                <div className="flex items-center gap-2.5">
                  <label
                    htmlFor={`${baseId}-blueprint`}
                    className="text-xs font-medium text-zinc-600 dark:text-zinc-400"
                  >
                    Blueprint view
                  </label>
                  <button
                    id={`${baseId}-blueprint`}
                    type="button"
                    role="switch"
                    aria-checked={blueprint}
                    onClick={() => setBlueprint((v) => !v)}
                    className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950 ${
                      blueprint
                        ? "bg-sky-600 dark:bg-sky-500"
                        : "bg-zinc-300 dark:bg-zinc-700"
                    }`}
                  >
                    <span
                      aria-hidden="true"
                      className={`inline-block h-[1.125rem] w-[1.125rem] transform rounded-full bg-white shadow transition-transform ${
                        blueprint ? "translate-x-[1.4rem]" : "translate-x-[0.2rem]"
                      }`}
                    />
                  </button>
                </div>
              </div>

              <div
                role="tablist"
                aria-label="Build phases"
                aria-orientation="horizontal"
                className="relative mt-6 flex gap-1 rounded-xl bg-zinc-100 p-1 dark:bg-zinc-800/70"
              >
                {PHASES.map((phase, index) => {
                  const selected = index === activeIndex;
                  return (
                    <button
                      key={phase.id}
                      ref={(node) => {
                        tabRefs.current[index] = node;
                      }}
                      id={`${baseId}-tab-${phase.id}`}
                      type="button"
                      role="tab"
                      aria-selected={selected}
                      aria-controls={`${baseId}-panel-${phase.id}`}
                      tabIndex={selected ? 0 : -1}
                      onClick={() => setActiveIndex(index)}
                      onKeyDown={onTabKeyDown}
                      className={`relative flex-1 rounded-lg px-3 py-2 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 sm:text-sm dark:focus-visible:ring-offset-zinc-800 ${
                        selected
                          ? "text-zinc-900 dark:text-white"
                          : "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
                      }`}
                    >
                      {selected ? (
                        <motion.span
                          layoutId={`${baseId}-tab-pill`}
                          className="absolute inset-0 rounded-lg bg-white shadow-sm dark:bg-zinc-700"
                          transition={
                            reduceMotion
                              ? { duration: 0 }
                              : { type: "spring", stiffness: 420, damping: 34 }
                          }
                        />
                      ) : null}
                      <span className="relative">{phase.label}</span>
                    </button>
                  );
                })}
              </div>

              {PHASES.map((phase, index) => (
                <div
                  key={phase.id}
                  id={`${baseId}-panel-${phase.id}`}
                  role="tabpanel"
                  aria-labelledby={`${baseId}-tab-${phase.id}`}
                  tabIndex={0}
                  hidden={index !== activeIndex}
                  className="relative mt-6 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500"
                >
                  <div className="flex flex-wrap items-center justify-between gap-3">
                    <StatusChip status={phase.status} />
                    <span className="text-xs font-medium tabular-nums text-zinc-500 dark:text-zinc-500">
                      {phase.percent}% done
                    </span>
                  </div>

                  <div
                    role="progressbar"
                    aria-valuenow={phase.percent}
                    aria-valuemin={0}
                    aria-valuemax={100}
                    aria-label={`${phase.label} progress`}
                    className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-800"
                  >
                    <motion.div
                      key={`${phase.id}-bar-${activeIndex}`}
                      className={`h-full rounded-full ${
                        phase.status === "complete"
                          ? "bg-emerald-500 dark:bg-emerald-400"
                          : phase.status === "active"
                            ? "bg-amber-500 dark:bg-amber-400"
                            : "bg-zinc-400 dark:bg-zinc-600"
                      }`}
                      initial={reduceMotion ? false : { width: 0 }}
                      animate={{ width: `${phase.percent}%` }}
                      transition={
                        reduceMotion
                          ? { duration: 0 }
                          : { duration: 0.7, ease: [0.22, 1, 0.36, 1] }
                      }
                      style={
                        reduceMotion ? { width: `${phase.percent}%` } : undefined
                      }
                    />
                  </div>

                  <p className="mt-5 text-sm font-semibold text-zinc-900 dark:text-white">
                    {phase.headline}
                  </p>
                  <p className="mt-2 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
                    {phase.note}
                  </p>

                  <ul className="mt-5 space-y-2.5">
                    {phase.items.map((item) => (
                      <li
                        key={item}
                        className="flex items-start gap-2.5 text-sm text-zinc-700 dark:text-zinc-300"
                      >
                        <span
                          aria-hidden="true"
                          className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${
                            phase.status === "complete"
                              ? "bg-emerald-500 dark:bg-emerald-400"
                              : phase.status === "active"
                                ? "bg-amber-500 dark:bg-amber-400"
                                : "bg-zinc-400 dark:bg-zinc-600"
                          }`}
                        />
                        {item}
                      </li>
                    ))}
                  </ul>
                </div>
              ))}

              <p className="relative mt-7 border-t border-zinc-200 pt-4 text-xs text-zinc-500 dark:border-zinc-800 dark:text-zinc-500">
                Phase {activeIndex + 1} of {PHASES.length} ·{" "}
                <span className="text-zinc-700 dark:text-zinc-300">
                  {activePhase.label}
                </span>{" "}
                · use the arrow keys to walk the site
              </p>
            </div>
          </div>
        </div>

        <div
          aria-hidden="true"
          className="euc-stripes mt-14 h-2.5 w-full rounded-full text-amber-400 lg:mt-20 dark:text-amber-300/80"
        />
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →