Empty No Connection
Original · freeno connection empty state
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/empty-no-connection.json"use client";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Status = "offline" | "checking" | "online";
type CheckState = "idle" | "checking" | "pass" | "fail" | "blocked";
type CheckId = "network" | "edge" | "socket";
type Checks = Record<CheckId, CheckState>;
type Probe = {
id: CheckId;
label: string;
detail: string;
};
type QueuedItem = {
id: string;
title: string;
where: string;
when: string;
weight: string;
};
const PROBES: Probe[] = [
{
id: "network",
label: "Device network",
detail: "Wi-Fi or cellular route out of this machine",
},
{
id: "edge",
label: "Edge region fra1",
detail: "edge.quayside.app responds to a health ping",
},
{
id: "socket",
label: "Realtime socket",
detail: "wss://rt.quayside.app/v3 accepts the session token",
},
];
const QUEUED: QueuedItem[] = [
{
id: "q1",
title: "Renamed “Q3 pricing” to “Q3 pricing v2”",
where: "Document",
when: "2 min ago",
weight: "1 edit",
},
{
id: "q2",
title: "Left 14 comments on the onboarding flow",
where: "Canvas",
when: "6 min ago",
weight: "14 edits",
},
{
id: "q3",
title: "Uploaded harbour-map.svg",
where: "Assets",
when: "9 min ago",
weight: "240 KB",
},
];
const IDLE_CHECKS: Checks = { network: "idle", edge: "idle", socket: "idle" };
const PASSED_CHECKS: Checks = { network: "pass", edge: "pass", socket: "pass" };
const STEP_MS = 620;
const TOTAL_EDITS = 16;
function backoffFor(attempts: number): number {
return Math.min(30, 5 * 2 ** Math.min(attempts, 3));
}
function arcClass(status: Status): string {
if (status === "online") return "stroke-emerald-500 dark:stroke-emerald-400";
if (status === "checking") return "stroke-sky-500 dark:stroke-sky-400";
return "stroke-slate-300 dark:stroke-slate-700";
}
function SpinnerIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeWidth={2.5}
opacity={0.25}
/>
<path
d="M21 12a9 9 0 00-9-9"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
/>
</svg>
);
}
function RetryIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M20 12a8 8 0 11-2.34-5.66" />
<path d="M20 4v4.5h-4.5" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M5 12.5l4.5 4.5L19 7" />
</svg>
);
}
function CrossIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
aria-hidden="true"
className={className}
>
<path d="M7 7l10 10M17 7L7 17" />
</svg>
);
}
function DashIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
aria-hidden="true"
className={className}
>
<path d="M6 12h12" />
</svg>
);
}
function ChevronIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M6 9l6 6 6-6" />
</svg>
);
}
function LifebuoyIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="3.75" />
<path d="M5.6 5.6l3.75 3.75M14.65 14.65l3.75 3.75M18.4 5.6l-3.75 3.75M9.35 14.65L5.6 18.4" />
</svg>
);
}
function ProbeGlyph({ state }: { state: CheckState }) {
if (state === "pass") {
return (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500 text-white">
<CheckIcon className="h-2.5 w-2.5" />
</span>
);
}
if (state === "fail") {
return (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-rose-500 text-white">
<CrossIcon className="h-2.5 w-2.5" />
</span>
);
}
if (state === "checking") {
return (
<span className="flex h-5 w-5 items-center justify-center text-sky-500 dark:text-sky-400">
<SpinnerIcon className="encon-spin h-4 w-4" />
</span>
);
}
if (state === "blocked") {
return (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-slate-200 text-slate-500 dark:bg-slate-800 dark:text-slate-500">
<DashIcon className="h-2.5 w-2.5" />
</span>
);
}
return (
<span
aria-hidden="true"
className="flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-slate-300 dark:border-slate-700"
/>
);
}
function probeStateLabel(state: CheckState): string {
if (state === "pass") return "Reachable";
if (state === "fail") return "No route";
if (state === "checking") return "Testing";
if (state === "blocked") return "Skipped";
return "Not tested";
}
export default function EmptyNoConnection() {
const reduce = useReducedMotion();
const headingId = useId();
const statusId = useId();
const autoId = useId();
const probesId = useId();
const queueId = useId();
const explainerId = useId();
const [status, setStatus] = useState<Status>("offline");
const [checks, setChecks] = useState<Checks>(IDLE_CHECKS);
const [attempts, setAttempts] = useState<number>(0);
const [auto, setAuto] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number | null>(null);
const [explainerOpen, setExplainerOpen] = useState<boolean>(false);
const timers = useRef<number[]>([]);
const clearTimers = useCallback(() => {
timers.current.forEach((id) => window.clearTimeout(id));
timers.current = [];
}, []);
useEffect(() => clearTimers, [clearTimers]);
const runCheck = useCallback(() => {
clearTimers();
setCountdown(null);
setStatus("checking");
setChecks(IDLE_CHECKS);
const reachable = navigator.onLine;
PROBES.forEach((probe, i) => {
timers.current.push(
window.setTimeout(() => {
setChecks((prev) => ({ ...prev, [probe.id]: "checking" }));
}, i * STEP_MS + 100),
);
timers.current.push(
window.setTimeout(
() => {
setChecks((prev) => ({
...prev,
[probe.id]: reachable ? "pass" : i === 0 ? "fail" : "blocked",
}));
},
i * STEP_MS + STEP_MS - 120,
),
);
});
timers.current.push(
window.setTimeout(() => {
if (reachable) {
setStatus("online");
setAttempts(0);
} else {
setStatus("offline");
setAttempts((n) => n + 1);
}
}, PROBES.length * STEP_MS + 160),
);
}, [clearTimers]);
const goOffline = useCallback(() => {
clearTimers();
setStatus("offline");
setChecks(IDLE_CHECKS);
setAttempts(0);
setCountdown(null);
}, [clearTimers]);
useEffect(() => {
const handleOnline = () => {
clearTimers();
setStatus("online");
setChecks(PASSED_CHECKS);
setAttempts(0);
setCountdown(null);
};
const handleOffline = () => {
clearTimers();
setStatus("offline");
setChecks(IDLE_CHECKS);
setCountdown(null);
};
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, [clearTimers]);
const wait = backoffFor(attempts);
useEffect(() => {
if (!auto || status !== "offline") return;
const deadline = Date.now() + wait * 1000;
const id = window.setInterval(() => {
const left = Math.ceil((deadline - Date.now()) / 1000);
if (left > 0) {
setCountdown(left);
return;
}
window.clearInterval(id);
runCheck();
}, 200);
return () => window.clearInterval(id);
}, [auto, status, wait, runCheck]);
const announcement = useMemo<string>(() => {
if (status === "checking") return "Testing the connection.";
if (status === "online")
return `Back online. ${TOTAL_EDITS} queued changes synced to region fra1.`;
if (attempts > 0)
return `Attempt ${attempts} failed. Still offline. ${TOTAL_EDITS} changes held on this device.`;
return `You are offline. ${TOTAL_EDITS} changes are held on this device.`;
}, [status, attempts]);
const fade = reduce
? { initial: false as const, animate: { opacity: 1 } }
: {
initial: { opacity: 0, y: 6 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -4 },
transition: { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const },
};
const busy = status === "checking";
const online = status === "online";
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 sm:py-28 dark:bg-slate-950 dark:text-slate-100"
>
<style>{`
@keyframes encon-spin { to { transform: rotate(360deg); } }
@keyframes encon-arc {
0%, 100% { opacity: .3; }
50% { opacity: 1; }
}
@keyframes encon-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes encon-ring {
0% { transform: scale(.85); opacity: .45; }
100% { transform: scale(1.4); opacity: 0; }
}
@keyframes encon-blip {
0%, 100% { opacity: .35; }
50% { opacity: .9; }
}
.encon-spin { animation: encon-spin 900ms linear infinite; }
.encon-arc { animation: encon-arc 1.4s ease-in-out infinite; }
.encon-float { animation: encon-float 6.5s ease-in-out infinite; }
.encon-ring { animation: encon-ring 2.6s ease-out infinite; }
.encon-blip { animation: encon-blip 2.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.encon-spin, .encon-arc, .encon-float, .encon-ring, .encon-blip {
animation: none !important;
}
}
`}</style>
<div
aria-hidden="true"
className={`pointer-events-none absolute left-1/2 top-0 h-72 w-[44rem] -translate-x-1/2 rounded-full blur-3xl transition-colors duration-700 ${
online
? "bg-emerald-400/10 dark:bg-emerald-500/10"
: "bg-slate-400/10 dark:bg-slate-500/10"
}`}
/>
<p id={statusId} role="status" aria-live="polite" className="sr-only">
{announcement}
</p>
<div className="relative mx-auto max-w-5xl">
{/* Illustration + headline */}
<div className="flex flex-col items-center text-center">
<div className="relative">
{busy ? (
<>
<span
aria-hidden="true"
className="encon-ring absolute left-1/2 top-[58%] h-28 w-28 -translate-x-1/2 -translate-y-1/2 rounded-full border border-sky-400/50"
/>
<span
aria-hidden="true"
className="encon-ring absolute left-1/2 top-[58%] h-28 w-28 -translate-x-1/2 -translate-y-1/2 rounded-full border border-sky-400/50"
style={{ animationDelay: "1.3s" }}
/>
</>
) : null}
<svg
viewBox="0 0 200 132"
role="img"
aria-label={
online
? "Signal restored"
: "A wireless signal icon struck through, meaning no connection"
}
className="encon-float relative h-32 w-48 sm:h-36 sm:w-56"
>
<g aria-hidden="true">
<circle
cx="100"
cy="88"
r="44"
className="stroke-slate-200 dark:stroke-slate-800"
strokeWidth={1}
strokeDasharray="3 7"
fill="none"
/>
<circle
cx="100"
cy="88"
r="56"
className="encon-blip stroke-slate-200 dark:stroke-slate-800"
strokeWidth={1}
strokeDasharray="3 9"
fill="none"
/>
</g>
<g
fill="none"
strokeLinecap="round"
className={arcClass(status)}
strokeWidth={5}
>
{[
{ d: "M74.5 62.5A36 36 0 0 1 125.5 62.5", delay: "0s" },
{ d: "M83 71A24 24 0 0 1 117 71", delay: "0.16s" },
{ d: "M91.5 79.5A12 12 0 0 1 108.5 79.5", delay: "0.32s" },
].map((arc) => (
<path
key={arc.d}
d={arc.d}
strokeDasharray={status === "offline" ? "5 7" : undefined}
className={busy ? "encon-arc" : undefined}
style={busy ? { animationDelay: arc.delay } : undefined}
/>
))}
</g>
<circle
cx="100"
cy="89"
r="4.5"
className={
online
? "fill-emerald-500 dark:fill-emerald-400"
: status === "checking"
? "fill-sky-500 dark:fill-sky-400"
: "fill-slate-300 dark:fill-slate-700"
}
/>
<motion.g
aria-hidden="true"
initial={false}
animate={{ opacity: online ? 0 : 1 }}
transition={reduce ? { duration: 0 } : { duration: 0.3 }}
>
<motion.path
d="M74 58L126 110"
className="stroke-slate-50 dark:stroke-slate-950"
strokeWidth={9}
strokeLinecap="round"
fill="none"
initial={reduce ? false : { pathLength: 0 }}
animate={{ pathLength: online ? 0 : 1 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.5, ease: [0.22, 1, 0.36, 1] }
}
/>
<motion.path
d="M74 58L126 110"
className="stroke-rose-500 dark:stroke-rose-400"
strokeWidth={3.5}
strokeLinecap="round"
fill="none"
initial={reduce ? false : { pathLength: 0 }}
animate={{ pathLength: online ? 0 : 1 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.5, ease: [0.22, 1, 0.36, 1] }
}
/>
</motion.g>
</svg>
</div>
<span
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium uppercase tracking-wider ${
online
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300"
: busy
? "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/10 dark:text-sky-300"
: "border-slate-200 bg-white text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400"
}`}
>
<span
aria-hidden="true"
className={`h-1.5 w-1.5 rounded-full ${
online
? "bg-emerald-500"
: busy
? "encon-blip bg-sky-500"
: "bg-rose-500"
}`}
/>
{online ? "Connected" : busy ? "Testing route" : "Connection lost"}
</span>
<h2
id={headingId}
className="mt-4 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white"
>
{online ? "Back online" : "Quayside can’t reach the server"}
</h2>
<p className="mt-3 max-w-lg text-sm leading-relaxed text-slate-600 sm:text-base dark:text-slate-400">
{online ? (
<>
Reconnected to region fra1. All {TOTAL_EDITS} queued changes
uploaded — your document matches what your team sees.
</>
) : (
<>
Your last {TOTAL_EDITS} changes are saved on this device and will
upload the moment a route opens. Nothing has been lost, and you
can keep editing offline.
</>
)}
</p>
<div className="mt-7 flex flex-col items-center gap-4">
<div className="flex flex-wrap items-center justify-center gap-3">
{online ? (
<button
type="button"
onClick={goOffline}
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition hover:border-slate-400 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-slate-600 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
>
<RetryIcon className="h-4 w-4" />
Replay the offline state
</button>
) : (
<button
type="button"
onClick={runCheck}
disabled={busy}
aria-describedby={statusId}
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-60 dark:focus-visible:ring-offset-slate-950"
>
{busy ? (
<SpinnerIcon className="encon-spin h-4 w-4" />
) : (
<RetryIcon className="h-4 w-4" />
)}
{busy ? "Testing connection…" : "Retry now"}
</button>
)}
<a
href="https://quayside.app/help/offline-mode"
target="_blank"
rel="noopener"
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
>
<LifebuoyIcon className="h-4 w-4" />
Connection help
</a>
</div>
<div className="flex items-center gap-3">
<button
id={autoId}
type="button"
role="switch"
aria-checked={auto}
onClick={() => {
setCountdown(null);
setAuto((v) => !v);
}}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 ${
auto
? "border-indigo-600 bg-indigo-600"
: "border-slate-300 bg-slate-200 dark:border-slate-700 dark:bg-slate-800"
}`}
>
<span
aria-hidden="true"
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
auto ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<label
htmlFor={autoId}
className="cursor-pointer select-none text-sm text-slate-600 dark:text-slate-400"
>
Keep retrying in the background
</label>
</div>
<div className="h-5">
<AnimatePresence initial={false} mode="wait">
{auto && countdown !== null && countdown > 0 && !online ? (
<motion.p
key="countdown"
{...fade}
className="text-xs tabular-nums text-slate-500 dark:text-slate-400"
>
Next attempt in {countdown}s · backoff {wait}s
</motion.p>
) : attempts > 0 && status === "offline" ? (
<motion.p
key="attempts"
{...fade}
className="text-xs text-rose-600 dark:text-rose-400"
>
{attempts} {attempts === 1 ? "attempt" : "attempts"} failed —
no route to fra1 from this device
</motion.p>
) : null}
</AnimatePresence>
</div>
</div>
</div>
{/* Diagnostics + queue */}
<div className="mt-10 grid gap-5 md:grid-cols-2">
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
<h3
id={probesId}
className="text-sm font-semibold text-slate-900 dark:text-white"
>
What we checked
</h3>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
Three hops between your browser and the workspace.
</p>
<ul aria-labelledby={probesId} className="mt-4 space-y-1">
{PROBES.map((probe, i) => {
const state = checks[probe.id];
return (
<motion.li
key={probe.id}
initial={reduce ? false : { opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.3, delay: 0.05 + i * 0.06 }
}
className="flex items-start gap-3 rounded-xl px-2 py-2.5"
>
<span className="mt-0.5 shrink-0">
<ProbeGlyph state={state} />
</span>
<span className="min-w-0 flex-1">
<span className="flex flex-wrap items-baseline justify-between gap-x-3">
<span className="text-sm font-medium text-slate-900 dark:text-slate-100">
{probe.label}
</span>
<span
className={`text-xs font-medium ${
state === "pass"
? "text-emerald-600 dark:text-emerald-400"
: state === "fail"
? "text-rose-600 dark:text-rose-400"
: state === "checking"
? "text-sky-600 dark:text-sky-400"
: "text-slate-400 dark:text-slate-500"
}`}
>
{probeStateLabel(state)}
</span>
</span>
<span className="mt-0.5 block break-words text-xs text-slate-500 dark:text-slate-400">
{probe.detail}
</span>
</span>
</motion.li>
);
})}
</ul>
<div className="mt-4 border-t border-slate-200 pt-4 dark:border-slate-800">
<button
type="button"
onClick={() => setExplainerOpen((v) => !v)}
aria-expanded={explainerOpen}
aria-controls={explainerId}
className="flex w-full items-center justify-between gap-2 rounded-lg px-1 py-1 text-left text-xs font-semibold text-slate-700 transition hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Why does a failed hop stop the rest?
<ChevronIcon
className={`h-4 w-4 shrink-0 transition-transform ${
explainerOpen ? "rotate-180" : ""
}`}
/>
</button>
<AnimatePresence initial={false}>
{explainerOpen ? (
<motion.div
key="explainer"
id={explainerId}
initial={reduce ? false : { height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.26, ease: [0.22, 1, 0.36, 1] }
}
className="overflow-hidden"
>
<p className="px-1 pb-1 pt-2 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
The hops are ordered. If your device has no route out,
pinging fra1 would only time out for 30 seconds and tell us
nothing new, so we skip ahead and hold your edits instead.
</p>
</motion.div>
) : null}
</AnimatePresence>
</div>
</div>
<div className="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-baseline justify-between gap-2">
<h3
id={queueId}
className="text-sm font-semibold text-slate-900 dark:text-white"
>
Waiting to upload
</h3>
<span
className={`rounded-md px-2 py-0.5 text-xs font-medium tabular-nums ${
online
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-500/15 dark:text-emerald-300"
: "bg-amber-100 text-amber-800 dark:bg-amber-500/15 dark:text-amber-300"
}`}
>
{online ? "All synced" : `${TOTAL_EDITS} changes`}
</span>
</div>
<ul aria-labelledby={queueId} className="mt-4 space-y-2">
{QUEUED.map((item) => (
<li
key={item.id}
className="flex items-start gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40"
>
<span className="mt-0.5 shrink-0">
{online ? (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500 text-white">
<CheckIcon className="h-2.5 w-2.5" />
</span>
) : (
<span
aria-hidden="true"
className="flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-amber-400 text-amber-500 dark:border-amber-500/60"
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
</span>
)}
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-slate-900 dark:text-slate-100">
{item.title}
</span>
<span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
{item.where} · {item.when} · {item.weight} ·{" "}
<span
className={
online
? "text-emerald-600 dark:text-emerald-400"
: "text-amber-600 dark:text-amber-400"
}
>
{online ? "Synced" : "Held locally"}
</span>
</span>
</span>
</li>
))}
</ul>
<p className="mt-auto pt-5 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
{online
? "Last synced just now · Region fra1 · Client v3.8.2"
: "Last synced 11:42 (9 minutes ago) · Region fra1 · Client v3.8.2"}
</p>
</div>
</div>
</div>
</section>
);
}Dependencies
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 quoteSimilar components
Browse all →
Empty No Data
Originalempty data state with illustration

Empty No Results
Originalno results with clear filters

Empty Cart
Originalempty shopping cart state

Empty Inbox
Originalempty inbox / all caught up

Empty First Run
Originalfirst-run get started empty state

Empty Notifications
Originalno notifications state

Empty Error
Originalsomething went wrong empty state

Empty Folder
Originalempty folder with upload CTA

Empty Success
Originalsuccess / done empty state

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

