12 / 20
All 20 in 20 Tailwind CSS Empty States
- 01Tailwind CSS No Search Results Empty State
- 02Tailwind CSS Onboarding Empty State with Setup Checklist
- 03Tailwind CSS Empty Shopping Cart Page
- 04Tailwind CSS Inbox Zero / All Caught Up Empty State
- 05Tailwind CSS Empty Table State (No Rows Found)
- 06Tailwind CSS Filter No Matches Empty State (Pure CSS)
- 07Tailwind CSS Empty Dashboard / No Data State
- 08Tailwind CSS 500 Internal Server Error Page
- 09Tailwind CSS Empty Wishlist / Favorites Page
- 10Tailwind CSS Empty Notification Dropdown Panel
- 11Tailwind CSS Error State with Retry Button
- 12Tailwind CSS Offline / No Internet Connection Page
- 13Tailwind CSS 403 Forbidden / Access Denied Page
- 14Tailwind CSS 404 Page Not Found Template
- 15Tailwind CSS Empty Trash / Archived Items State
- 16Tailwind CSS Skeleton Loader to Empty State Transition
- 17Accessible Empty State in Tailwind CSS (ARIA Live Region)
- 18Pure Tailwind CSS Illustrated Empty State (No Images)
- 19Tailwind CSS Dark Mode Empty State (dark: Variant)
- 20Tailwind CSS Inline Micro Empty State Component
Tailwind CSS Offline / No Internet Connection Page
A notes app that has lost its connection and says exactly what that means: edits are safe on this device, three are queued, nothing will be overwritten. The queue rows tick to "synced" one by one when the connection returns — and the demo listens to the browser's real online/offline events, so it reacts to your network too.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="tes-12 group/root w-full min-h-svh bg-tes12-bg font-tes12-sans text-tes12-ink antialiased">
<div class="grid min-h-svh md:grid-cols-[14rem_minmax(0,1fr)]">
<aside class="flex flex-col gap-5 border-b border-tes12-line bg-tes12-side p-4 md:border-r md:border-b-0">
<span class="px-2 font-tes12-serif text-2xl">Quill</span>
<nav class="hidden gap-0.5 text-sm md:grid">
<a href="#" aria-current="page" class="flex min-h-10 items-center rounded-lg bg-tes12-card px-3 font-medium shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">All notes</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Shared with me</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Templates</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Trash</a>
</nav>
<p role="status" class="mt-auto flex items-center gap-2 rounded-full bg-tes12-warn/12 px-3 py-2 text-xs font-medium text-tes12-warn group-data-online/root:bg-tes12-ok/12 group-data-online/root:text-tes12-ok"><i aria-hidden="true" class="size-2 rounded-full bg-current group-data-busy/root:animate-pulse"></i><span id="tes12-conn">Offline</span></p>
</aside>
<main class="grid place-items-center px-[clamp(1rem,5vw,3rem)] py-12">
<div class="grid w-full max-w-lg gap-6">
<span aria-hidden="true" class="relative grid h-20 w-24 place-items-center overflow-hidden rounded-2xl bg-tes12-card text-tes12-warn ring-1 ring-tes12-line group-data-online/root:text-tes12-ok">
<svg class="size-10 animate-tes12-drift group-data-online/root:hidden motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18zM4 4l16 16"/></svg>
<svg class="hidden size-10 group-data-online/root:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18z"/><path d="m9.5 13.5 2 2 3.5-3.5"/></svg>
</span>
<div class="grid gap-2">
<h1 id="tes12-h" class="font-tes12-serif text-[clamp(2rem,4.5vw,2.8rem)] leading-[1.05] tracking-tight text-balance">You are working offline</h1>
<p id="tes12-p" class="text-[15px]/7 text-tes12-mut text-pretty">Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.</p>
</div>
<ul class="divide-y divide-tes12-line overflow-hidden rounded-2xl bg-tes12-card ring-1 ring-tes12-line">
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Q3 supplier review</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 4 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Warehouse handover</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 11 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Interview notes — Dee O.</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">created offline</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
</ul>
<div class="flex flex-wrap gap-2.5">
<button type="button" id="tes12-retry" class="min-h-11 rounded-full bg-tes12-ink px-5 text-sm font-medium text-white transition hover:bg-tes12-acc active:scale-[.98] disabled:opacity-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Check connection</button>
<button type="button" class="min-h-11 rounded-full border border-tes12-line2 px-5 text-sm hover:bg-tes12-side focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Keep writing offline</button>
</div>
</div>
</main>
</div>
</div><div class="tes-12 group/root w-full min-h-svh bg-tes12-bg font-tes12-sans text-tes12-ink antialiased">
<div class="grid min-h-svh md:grid-cols-[14rem_minmax(0,1fr)]">
<aside class="flex flex-col gap-5 border-b border-tes12-line bg-tes12-side p-4 md:border-r md:border-b-0">
<span class="px-2 font-tes12-serif text-2xl">Quill</span>
<nav class="hidden gap-0.5 text-sm md:grid">
<a href="#" aria-current="page" class="flex min-h-10 items-center rounded-lg bg-tes12-card px-3 font-medium shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">All notes</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Shared with me</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Templates</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Trash</a>
</nav>
<p role="status" class="mt-auto flex items-center gap-2 rounded-full bg-tes12-warn/12 px-3 py-2 text-xs font-medium text-tes12-warn group-data-online/root:bg-tes12-ok/12 group-data-online/root:text-tes12-ok"><i aria-hidden="true" class="size-2 rounded-full bg-current group-data-busy/root:animate-pulse"></i><span id="tes12-conn">Offline</span></p>
</aside>
<main class="grid place-items-center px-[clamp(1rem,5vw,3rem)] py-12">
<div class="grid w-full max-w-lg gap-6">
<span aria-hidden="true" class="relative grid h-20 w-24 place-items-center overflow-hidden rounded-2xl bg-tes12-card text-tes12-warn ring-1 ring-tes12-line group-data-online/root:text-tes12-ok">
<svg class="size-10 animate-tes12-drift group-data-online/root:hidden motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18zM4 4l16 16"/></svg>
<svg class="hidden size-10 group-data-online/root:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18z"/><path d="m9.5 13.5 2 2 3.5-3.5"/></svg>
</span>
<div class="grid gap-2">
<h1 id="tes12-h" class="font-tes12-serif text-[clamp(2rem,4.5vw,2.8rem)] leading-[1.05] tracking-tight text-balance">You are working offline</h1>
<p id="tes12-p" class="text-[15px]/7 text-tes12-mut text-pretty">Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.</p>
</div>
<ul class="divide-y divide-tes12-line overflow-hidden rounded-2xl bg-tes12-card ring-1 ring-tes12-line">
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Q3 supplier review</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 4 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Warehouse handover</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 11 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Interview notes — Dee O.</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">created offline</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
</ul>
<div class="flex flex-wrap gap-2.5">
<button type="button" id="tes12-retry" class="min-h-11 rounded-full bg-tes12-ink px-5 text-sm font-medium text-white transition hover:bg-tes12-acc active:scale-[.98] disabled:opacity-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Check connection</button>
<button type="button" class="min-h-11 rounded-full border border-tes12-line2 px-5 text-sm hover:bg-tes12-side focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Keep writing offline</button>
</div>
</div>
</main>
</div>
</div>@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}(() => {
const root = document.querySelector('.tes-12');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const $ = (s) => root.querySelector(s);
const retry = $('#tes12-retry'), conn = $('#tes12-conn');
const rows = [...root.querySelectorAll('[data-q]')];
const goOnline = () => {
root.removeAttribute('data-busy'); root.setAttribute('data-online', '');
conn.textContent = 'Online';
$('#tes12-h').textContent = 'Back online, everything synced';
$('#tes12-p').textContent = 'All three edits reached the server in order. Nothing needed merging.';
rows.forEach((r, i) => setTimeout(() => r.setAttribute('data-synced', ''), 220 * (i + 1)));
retry.disabled = false; retry.textContent = 'Open Q3 supplier review';
};
const goOffline = () => {
root.removeAttribute('data-online'); root.removeAttribute('data-busy');
conn.textContent = 'Offline';
$('#tes12-h').textContent = 'You are working offline';
$('#tes12-p').textContent = 'Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.';
rows.forEach((r) => r.removeAttribute('data-synced'));
retry.disabled = false; retry.textContent = 'Check connection';
};
retry.addEventListener('click', () => {
if (root.hasAttribute('data-online')) return;
retry.disabled = true; retry.textContent = 'Reconnecting\u2026';
root.setAttribute('data-busy', ''); conn.textContent = 'Reconnecting';
setTimeout(() => {
if (navigator.onLine === false) { goOffline(); conn.textContent = 'Still offline'; }
else goOnline();
}, 1400);
});
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
})();(() => {
const root = document.querySelector('.tes-12');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const $ = (s) => root.querySelector(s);
const retry = $('#tes12-retry'), conn = $('#tes12-conn');
const rows = [...root.querySelectorAll('[data-q]')];
const goOnline = () => {
root.removeAttribute('data-busy'); root.setAttribute('data-online', '');
conn.textContent = 'Online';
$('#tes12-h').textContent = 'Back online, everything synced';
$('#tes12-p').textContent = 'All three edits reached the server in order. Nothing needed merging.';
rows.forEach((r, i) => setTimeout(() => r.setAttribute('data-synced', ''), 220 * (i + 1)));
retry.disabled = false; retry.textContent = 'Open Q3 supplier review';
};
const goOffline = () => {
root.removeAttribute('data-online'); root.removeAttribute('data-busy');
conn.textContent = 'Offline';
$('#tes12-h').textContent = 'You are working offline';
$('#tes12-p').textContent = 'Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.';
rows.forEach((r) => r.removeAttribute('data-synced'));
retry.disabled = false; retry.textContent = 'Check connection';
};
retry.addEventListener('click', () => {
if (root.hasAttribute('data-online')) return;
retry.disabled = true; retry.textContent = 'Reconnecting\u2026';
root.setAttribute('data-busy', ''); conn.textContent = 'Reconnecting';
setTimeout(() => {
if (navigator.onLine === false) { goOffline(); conn.textContent = 'Still offline'; }
else goOnline();
}, 1400);
});
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..600&family=Instrument+Serif:ital@0;1&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..600&family=Instrument+Serif:ital@0;1&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.est-12__stage[data-online] .est-12__qstaterules becamegroup-data-synced/q:on each row andgroup-data-online/root:on the shared chrome.- UI upgrade from the CSS original: the demo now listens to real
online/offlineevents and refuses to fake a reconnection whennavigator.onLineis false. - UI upgrade: the plate swaps to a cloud-check when synced, a pulsing dot marks "Reconnecting", and queue rows tick over with a staggered check icon.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind CSS Offline / No Internet Connection Page
Source: https://codefronts.com/snippets/tailwind-css-empty-states/tailwind-css-offline-no-internet-connection-page/
A notes app that has lost its connection and says exactly what that means: edits are safe on this device, three are queued, nothing will be overwritten. The queue rows tick to "synced" one by one when the connection returns — and the demo listens to the browser's real online/offline events, so it reacts to your network too.
## HTML
```html
<div class="tes-12 group/root w-full min-h-svh bg-tes12-bg font-tes12-sans text-tes12-ink antialiased">
<div class="grid min-h-svh md:grid-cols-[14rem_minmax(0,1fr)]">
<aside class="flex flex-col gap-5 border-b border-tes12-line bg-tes12-side p-4 md:border-r md:border-b-0">
<span class="px-2 font-tes12-serif text-2xl">Quill</span>
<nav class="hidden gap-0.5 text-sm md:grid">
<a href="#" aria-current="page" class="flex min-h-10 items-center rounded-lg bg-tes12-card px-3 font-medium shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">All notes</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Shared with me</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Templates</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Trash</a>
</nav>
<p role="status" class="mt-auto flex items-center gap-2 rounded-full bg-tes12-warn/12 px-3 py-2 text-xs font-medium text-tes12-warn group-data-online/root:bg-tes12-ok/12 group-data-online/root:text-tes12-ok"><i aria-hidden="true" class="size-2 rounded-full bg-current group-data-busy/root:animate-pulse"></i><span id="tes12-conn">Offline</span></p>
</aside>
<main class="grid place-items-center px-[clamp(1rem,5vw,3rem)] py-12">
<div class="grid w-full max-w-lg gap-6">
<span aria-hidden="true" class="relative grid h-20 w-24 place-items-center overflow-hidden rounded-2xl bg-tes12-card text-tes12-warn ring-1 ring-tes12-line group-data-online/root:text-tes12-ok">
<svg class="size-10 animate-tes12-drift group-data-online/root:hidden motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18zM4 4l16 16"/></svg>
<svg class="hidden size-10 group-data-online/root:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18z"/><path d="m9.5 13.5 2 2 3.5-3.5"/></svg>
</span>
<div class="grid gap-2">
<h1 id="tes12-h" class="font-tes12-serif text-[clamp(2rem,4.5vw,2.8rem)] leading-[1.05] tracking-tight text-balance">You are working offline</h1>
<p id="tes12-p" class="text-[15px]/7 text-tes12-mut text-pretty">Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.</p>
</div>
<ul class="divide-y divide-tes12-line overflow-hidden rounded-2xl bg-tes12-card ring-1 ring-tes12-line">
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Q3 supplier review</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 4 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Warehouse handover</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 11 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Interview notes — Dee O.</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">created offline</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
</ul>
<div class="flex flex-wrap gap-2.5">
<button type="button" id="tes12-retry" class="min-h-11 rounded-full bg-tes12-ink px-5 text-sm font-medium text-white transition hover:bg-tes12-acc active:scale-[.98] disabled:opacity-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Check connection</button>
<button type="button" class="min-h-11 rounded-full border border-tes12-line2 px-5 text-sm hover:bg-tes12-side focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Keep writing offline</button>
</div>
</div>
</main>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tes-12');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const $ = (s) => root.querySelector(s);
const retry = $('#tes12-retry'), conn = $('#tes12-conn');
const rows = [...root.querySelectorAll('[data-q]')];
const goOnline = () => {
root.removeAttribute('data-busy'); root.setAttribute('data-online', '');
conn.textContent = 'Online';
$('#tes12-h').textContent = 'Back online, everything synced';
$('#tes12-p').textContent = 'All three edits reached the server in order. Nothing needed merging.';
rows.forEach((r, i) => setTimeout(() => r.setAttribute('data-synced', ''), 220 * (i + 1)));
retry.disabled = false; retry.textContent = 'Open Q3 supplier review';
};
const goOffline = () => {
root.removeAttribute('data-online'); root.removeAttribute('data-busy');
conn.textContent = 'Offline';
$('#tes12-h').textContent = 'You are working offline';
$('#tes12-p').textContent = 'Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.';
rows.forEach((r) => r.removeAttribute('data-synced'));
retry.disabled = false; retry.textContent = 'Check connection';
};
retry.addEventListener('click', () => {
if (root.hasAttribute('data-online')) return;
retry.disabled = true; retry.textContent = 'Reconnecting\u2026';
root.setAttribute('data-busy', ''); conn.textContent = 'Reconnecting';
setTimeout(() => {
if (navigator.onLine === false) { goOffline(); conn.textContent = 'Still offline'; }
else goOnline();
}, 1400);
});
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind CSS Offline / No Internet Connection Page
Source: https://codefronts.com/snippets/tailwind-css-empty-states/tailwind-css-offline-no-internet-connection-page/
A notes app that has lost its connection and says exactly what that means: edits are safe on this device, three are queued, nothing will be overwritten. The queue rows tick to "synced" one by one when the connection returns — and the demo listens to the browser's real online/offline events, so it reacts to your network too.
## HTML
```html
<div class="tes-12 group/root w-full min-h-svh bg-tes12-bg font-tes12-sans text-tes12-ink antialiased">
<div class="grid min-h-svh md:grid-cols-[14rem_minmax(0,1fr)]">
<aside class="flex flex-col gap-5 border-b border-tes12-line bg-tes12-side p-4 md:border-r md:border-b-0">
<span class="px-2 font-tes12-serif text-2xl">Quill</span>
<nav class="hidden gap-0.5 text-sm md:grid">
<a href="#" aria-current="page" class="flex min-h-10 items-center rounded-lg bg-tes12-card px-3 font-medium shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">All notes</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Shared with me</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Templates</a>
<a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-tes12-mut hover:bg-tes12-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Trash</a>
</nav>
<p role="status" class="mt-auto flex items-center gap-2 rounded-full bg-tes12-warn/12 px-3 py-2 text-xs font-medium text-tes12-warn group-data-online/root:bg-tes12-ok/12 group-data-online/root:text-tes12-ok"><i aria-hidden="true" class="size-2 rounded-full bg-current group-data-busy/root:animate-pulse"></i><span id="tes12-conn">Offline</span></p>
</aside>
<main class="grid place-items-center px-[clamp(1rem,5vw,3rem)] py-12">
<div class="grid w-full max-w-lg gap-6">
<span aria-hidden="true" class="relative grid h-20 w-24 place-items-center overflow-hidden rounded-2xl bg-tes12-card text-tes12-warn ring-1 ring-tes12-line group-data-online/root:text-tes12-ok">
<svg class="size-10 animate-tes12-drift group-data-online/root:hidden motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18zM4 4l16 16"/></svg>
<svg class="hidden size-10 group-data-online/root:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18h10a4 4 0 0 0 .6-7.95A6 6 0 0 0 6.2 9.1 4.5 4.5 0 0 0 7 18z"/><path d="m9.5 13.5 2 2 3.5-3.5"/></svg>
</span>
<div class="grid gap-2">
<h1 id="tes12-h" class="font-tes12-serif text-[clamp(2rem,4.5vw,2.8rem)] leading-[1.05] tracking-tight text-balance">You are working offline</h1>
<p id="tes12-p" class="text-[15px]/7 text-tes12-mut text-pretty">Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.</p>
</div>
<ul class="divide-y divide-tes12-line overflow-hidden rounded-2xl bg-tes12-card ring-1 ring-tes12-line">
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Q3 supplier review</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 4 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Warehouse handover</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">edited 11 min ago</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
<li data-q class="group/q flex items-center gap-3 px-4 py-3"><i aria-hidden="true" class="size-2 flex-none rounded-full bg-tes12-warn transition-colors group-data-synced/q:bg-tes12-ok"></i><span class="flex-1 text-sm font-medium">Interview notes — Dee O.</span><span class="text-xs text-tes12-mut group-data-synced/q:hidden">created offline</span><span class="hidden items-center gap-1 text-xs font-medium text-tes12-ok group-data-synced/q:inline-flex"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>synced</span></li>
</ul>
<div class="flex flex-wrap gap-2.5">
<button type="button" id="tes12-retry" class="min-h-11 rounded-full bg-tes12-ink px-5 text-sm font-medium text-white transition hover:bg-tes12-acc active:scale-[.98] disabled:opacity-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Check connection</button>
<button type="button" class="min-h-11 rounded-full border border-tes12-line2 px-5 text-sm hover:bg-tes12-side focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tes12-acc">Keep writing offline</button>
</div>
</div>
</main>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tes12-bg: #fbfaf7;
--color-tes12-bg: oklch(0.984 0.005 90);
--color-tes12-side: #f3f1ec;
--color-tes12-side: oklch(0.957 0.008 90);
--color-tes12-card: #ffffff;
--color-tes12-card: oklch(1 0 0);
--color-tes12-line: #e5e2da;
--color-tes12-line: oklch(0.91 0.01 90);
--color-tes12-line2: #cfcabe;
--color-tes12-line2: oklch(0.83 0.016 90);
--color-tes12-ink: #1e1d19;
--color-tes12-ink: oklch(0.22 0.008 90);
--color-tes12-mut: #5f5c53;
--color-tes12-mut: oklch(0.47 0.013 90);
--color-tes12-acc: #2d5bd0;
--color-tes12-acc: oklch(0.5 0.18 265);
--color-tes12-warn: #a2560a;
--color-tes12-warn: oklch(0.55 0.13 60);
--color-tes12-ok: #157a52;
--color-tes12-ok: oklch(0.52 0.11 160);
--font-tes12-sans: 'Geist', system-ui, sans-serif;
--font-tes12-serif: 'Instrument Serif', Georgia, serif;
--animate-tes12-drift: tes12-drift 4s ease-in-out infinite;
@keyframes tes12-drift {
0%,
100% {
translate: -3px 0;
}
50% {
translate: 3px -2px;
}
}
}
/* Root rule: the full-bleed contract, restated in CSS so the stage is sized before the browser
build compiles, plus a guard so the hidden attribute always beats display utilities. */
.tes-12 {
width: 100%;
min-height: 100svh;
}
.tes-12 [hidden] {
display: none !important;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tes-12');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const $ = (s) => root.querySelector(s);
const retry = $('#tes12-retry'), conn = $('#tes12-conn');
const rows = [...root.querySelectorAll('[data-q]')];
const goOnline = () => {
root.removeAttribute('data-busy'); root.setAttribute('data-online', '');
conn.textContent = 'Online';
$('#tes12-h').textContent = 'Back online, everything synced';
$('#tes12-p').textContent = 'All three edits reached the server in order. Nothing needed merging.';
rows.forEach((r, i) => setTimeout(() => r.setAttribute('data-synced', ''), 220 * (i + 1)));
retry.disabled = false; retry.textContent = 'Open Q3 supplier review';
};
const goOffline = () => {
root.removeAttribute('data-online'); root.removeAttribute('data-busy');
conn.textContent = 'Offline';
$('#tes12-h').textContent = 'You are working offline';
$('#tes12-p').textContent = 'Everything you type is saved on this device. Three edits are waiting to sync and nothing will be overwritten.';
rows.forEach((r) => r.removeAttribute('data-synced'));
retry.disabled = false; retry.textContent = 'Check connection';
};
retry.addEventListener('click', () => {
if (root.hasAttribute('data-online')) return;
retry.disabled = true; retry.textContent = 'Reconnecting\u2026';
root.setAttribute('data-busy', ''); conn.textContent = 'Reconnecting';
setTimeout(() => {
if (navigator.onLine === false) { goOffline(); conn.textContent = 'Still offline'; }
else goOnline();
}, 1400);
});
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
})();
```How this works
Offline is a mode, not an error. The page stays fully usable; only the sync pill and the queue change:
<li class="group/q" data-synced>
<span class="text-tes12-warn group-data-synced/q:text-tes12-ok">Honest states. "Check connection" shows "Reconnecting…" for real time before succeeding; if the browser reports navigator.onLine === false, the demo stays offline and says so.
Make it yours
- Hook up a real sync engine: call
markSynced(i)as each write is acknowledged. - Queue copy lives in the markup; add or remove rows freely.
- Offline hue:
--color-tes12-warn.
Gotchas — read before shipping
navigator.onLineonly knows about the local link, not whether your API is reachable — confirm with a lightweight request before syncing.- Announce reconnection through
role="status"; don't steal focus while the user is typing.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
online/offline events are universal.