05 / 20
All 20 in 20 CSS Empty States
- 01No Search Results State
- 02First-Time Onboarding State
- 03Empty Shopping Cart State
- 04Inbox Zero / All Caught Up State
- 05Empty Data Table / No Rows State
- 06Filtered List No Matches State
- 07Empty Dashboard / No Analytics Data
- 08500 Server Error Page Layout
- 09Empty Wishlist / Favorites State
- 10Empty Notification Center Flyout
- 11Failed to Load / Connection Error State
- 12Offline / No Internet Connection State
- 13Permission Denied / Restricted Access State
- 14404 Page Not Found Layout
- 15Deleted or Archived Items State
- 16Skeleton Loading to Empty State Transition
- 17Accessible Empty State (ARIA Live)
- 18Pure CSS Illustrated Empty State
- 19Dark Mode Compatible Empty State
- 20Inline / Micro Empty State Component
Empty Data Table / No Rows State
The empty table done properly: real column headers stay, a single colspan cell carries the message, and ghost rows fade in behind it so the grid still reads as a table rather than a hole. New invoice adds a genuine draft row and updates the footer count, so the demo covers the transition out of empty as well as the state itself.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="est-05">
<div class="est-05__page">
<header class="est-05__phead">
<div class="est-05__ptitle">
<span class="est-05__crumbs">Billing<span aria-hidden="true">/</span>Invoices</span>
<h1 class="est-05__h1">Invoices</h1>
</div>
<div class="est-05__phacts">
<button class="est-05__ghost" type="button">Export</button>
<button class="est-05__pri" type="button" id="est-05-new">New invoice</button>
</div>
</header>
<div class="est-05__toolbar">
<div class="est-05__seg" role="group" aria-label="Status">
<button class="est-05__segb est-05__segb--on" type="button">Unpaid</button>
<button class="est-05__segb" type="button">Paid</button>
<button class="est-05__segb" type="button">Draft</button>
<button class="est-05__segb" type="button">Void</button>
</div>
<label class="est-05__search">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.2" stroke="currentColor" stroke-width="1.7"/><path d="m15.8 15.8 3.6 3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<input type="search" placeholder="Client, number or amount" aria-label="Search invoices">
</label>
</div>
<div class="est-05__tablewrap">
<table class="est-05__table">
<thead>
<tr>
<th scope="col" class="est-05__thnum">Number</th>
<th scope="col">Client</th>
<th scope="col">Issued</th>
<th scope="col">Due</th>
<th scope="col" class="est-05__thr">Amount</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="est-05-body">
<tr class="est-05__emptyrow">
<td colspan="6">
<div class="est-05__ghosts" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="est-05__empty">
<span class="est-05__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="M6 3.5h9l3.5 3.5v13.5H6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14.6 3.8V7.4h3.6M9 12h6M9 15.5h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<p class="est-05__eh">Nothing unpaid</p>
<p class="est-05__ep">Every invoice issued this quarter has cleared. Drafts stay here until you send them.</p>
<div class="est-05__eacts">
<button class="est-05__pri est-05__pri--sm" type="button" id="est-05-new2">New invoice</button>
<button class="est-05__ghost est-05__ghost--sm" type="button">Import from CSV</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="est-05__pfoot">
<span class="est-05__pinfo" id="est-05-info">No rows</span>
<div class="est-05__pager">
<button class="est-05__pbtn" type="button" disabled aria-label="Previous page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m14 7-5 5 5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="est-05__pbtn" type="button" disabled aria-label="Next page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m10 7 5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</footer>
</div>
</div><div class="est-05">
<div class="est-05__page">
<header class="est-05__phead">
<div class="est-05__ptitle">
<span class="est-05__crumbs">Billing<span aria-hidden="true">/</span>Invoices</span>
<h1 class="est-05__h1">Invoices</h1>
</div>
<div class="est-05__phacts">
<button class="est-05__ghost" type="button">Export</button>
<button class="est-05__pri" type="button" id="est-05-new">New invoice</button>
</div>
</header>
<div class="est-05__toolbar">
<div class="est-05__seg" role="group" aria-label="Status">
<button class="est-05__segb est-05__segb--on" type="button">Unpaid</button>
<button class="est-05__segb" type="button">Paid</button>
<button class="est-05__segb" type="button">Draft</button>
<button class="est-05__segb" type="button">Void</button>
</div>
<label class="est-05__search">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.2" stroke="currentColor" stroke-width="1.7"/><path d="m15.8 15.8 3.6 3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<input type="search" placeholder="Client, number or amount" aria-label="Search invoices">
</label>
</div>
<div class="est-05__tablewrap">
<table class="est-05__table">
<thead>
<tr>
<th scope="col" class="est-05__thnum">Number</th>
<th scope="col">Client</th>
<th scope="col">Issued</th>
<th scope="col">Due</th>
<th scope="col" class="est-05__thr">Amount</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="est-05-body">
<tr class="est-05__emptyrow">
<td colspan="6">
<div class="est-05__ghosts" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="est-05__empty">
<span class="est-05__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="M6 3.5h9l3.5 3.5v13.5H6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14.6 3.8V7.4h3.6M9 12h6M9 15.5h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<p class="est-05__eh">Nothing unpaid</p>
<p class="est-05__ep">Every invoice issued this quarter has cleared. Drafts stay here until you send them.</p>
<div class="est-05__eacts">
<button class="est-05__pri est-05__pri--sm" type="button" id="est-05-new2">New invoice</button>
<button class="est-05__ghost est-05__ghost--sm" type="button">Import from CSV</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="est-05__pfoot">
<span class="est-05__pinfo" id="est-05-info">No rows</span>
<div class="est-05__pager">
<button class="est-05__pbtn" type="button" disabled aria-label="Previous page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m14 7-5 5 5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="est-05__pbtn" type="button" disabled aria-label="Next page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m10 7 5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</footer>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-05 {
--bg: #fbfbfc;
--surface: #ffffff;
--sunk: #f4f5f7;
--line: #e6e8ec;
--line-2: #d5d8de;
--ink: #14161b;
--muted: #5a5f6a;
--faint: #8b909a;
--accent: #1f3d7a;
--ok: #2c6a4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-05 *,
.est-05 *::before,
.est-05 *::after {
box-sizing: border-box;
}
.est-05 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-05 {
--bg: oklch(0.988 0.002 265);
--surface: oklch(1 0 0);
--sunk: oklch(0.968 0.004 265);
--line: oklch(0.923 0.006 265);
--line-2: oklch(0.872 0.008 265);
--ink: oklch(0.175 0.01 265);
--muted: oklch(0.465 0.012 265);
--faint: oklch(0.63 0.013 265);
--accent: oklch(0.38 0.095 262);
--ok: oklch(0.48 0.08 155);
}
}
.est-05__page {
inline-size: min(76rem,100%);
justify-self: center;
padding: clamp(1.25rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.15rem;
align-content: start;
}
.est-05__phead {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
}
.est-05__crumbs {
display: flex;
gap: .4rem;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--faint);
}
.est-05__h1 {
margin: .35rem 0 0;
font-size: clamp(1.5rem,3.4vw,2.1rem);
font-weight: 600;
letter-spacing: -.035em;
line-height: 1;
}
.est-05__phacts {
display: flex;
gap: .5rem;
}
.est-05__pri {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 9px;
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--accent) 80%, transparent);
transition: translate .16s, background .16s;
}
.est-05__pri:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-05__ghost {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-05__ghost:hover {
background: var(--sunk);
}
.est-05__pri--sm,
.est-05__ghost--sm {
min-block-size: 2.4rem;
padding: .5rem .85rem;
font-size: .82rem;
}
.est-05__toolbar {
display: flex;
flex-wrap: wrap;
gap: .6rem 1rem;
align-items: center;
justify-content: space-between;
}
.est-05__seg {
display: flex;
padding: .2rem;
background: var(--sunk);
border: 1px solid var(--line);
border-radius: 10px;
}
.est-05__segb {
padding: .42rem .75rem;
font: inherit;
font-size: .82rem;
font-weight: 500;
cursor: pointer;
color: var(--muted);
background: transparent;
border: 0;
border-radius: 7px;
transition: color .16s, background .16s;
}
.est-05__segb:hover {
color: var(--ink);
}
.est-05__segb--on {
color: var(--ink);
background: var(--surface);
box-shadow: 0 1px 2px rgba(20,22,27,.08);
}
.est-05__search {
display: flex;
align-items: center;
gap: .5rem;
flex: 1 1 16rem;
max-inline-size: 22rem;
padding: .5rem .7rem;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
}
.est-05__search svg {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
flex: none;
}
.est-05__search input {
flex: 1 1 auto;
min-inline-size: 0;
font: inherit;
font-size: .85rem;
color: var(--ink);
background: none;
border: 0;
outline: none;
}
.est-05__search:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 12%, transparent);
}
.est-05__tablewrap {
overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 1px 2px rgba(20,22,27,.04);
}
.est-05__table {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
min-inline-size: 44rem;
}
.est-05__table th {
position: sticky;
inset-block-start: 0;
z-index: 1;
text-align: start;
padding: .7rem 1rem;
background: var(--sunk);
border-block-end: 1px solid var(--line);
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .68rem;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--faint);
white-space: nowrap;
}
.est-05__thr {
text-align: end;
}
.est-05__thnum {
inline-size: 8rem;
}
.est-05__emptyrow td {
position: relative;
padding: 0;
}
.est-05__ghosts {
position: absolute;
inset: 0;
display: grid;
grid-auto-rows: 3.25rem;
overflow: hidden;
mask-image: linear-gradient(180deg,#000,transparent 85%);
}
.est-05__ghosts span {
border-block-end: 1px solid var(--line);
position: relative;
}
.est-05__ghosts span::before {
content: '';
position: absolute;
inset-block-start: 1.35rem;
inset-inline-start: 1rem;
block-size: .55rem;
inline-size: min(38%,14rem);
border-radius: 3px;
background: linear-gradient(90deg,var(--sunk),color-mix(in oklab,var(--line) 70%,transparent),var(--sunk));
background-size: 200% 100%;
animation: est-05-shim 4.5s linear infinite;
}
.est-05__ghosts span:nth-child(even)::before {
inline-size: min(26%,10rem);
animation-delay: .6s;
}
@keyframes est-05-shim {
to {
background-position: -200% 0;
}
}
.est-05__empty {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
text-align: center;
gap: .4rem;
padding: clamp(2.5rem,9vh,5rem) 1.25rem clamp(2rem,7vh,4rem);
animation: est-05-in .5s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-05-in {
from {
opacity: 0;
translate: 0 8px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-05__glyph {
inline-size: 3.5rem;
block-size: 3.5rem;
display: grid;
place-items: center;
margin-block-end: .55rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 7%, var(--surface));
border: 1px solid color-mix(in oklab, var(--accent) 18%, var(--line));
border-radius: 12px;
}
.est-05__glyph svg {
inline-size: 1.5rem;
block-size: 1.5rem;
}
.est-05__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
}
.est-05__ep {
margin: 0;
max-inline-size: 40ch;
font-size: .88rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-05__eacts {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
margin-block-start: .85rem;
}
.est-05__row td {
padding: .85rem 1rem;
border-block-end: 1px solid var(--line);
font-size: .87rem;
vertical-align: middle;
}
.est-05__row:last-child td {
border-block-end: 0;
}
.est-05__row:hover td {
background: var(--sunk);
}
.est-05__num {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
color: var(--muted);
}
.est-05__amt {
text-align: end;
font-variant-numeric: tabular-nums;
}
.est-05__st {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .2rem .5rem;
font-size: .74rem;
border-radius: 6px;
background: var(--sunk);
border: 1px solid var(--line);
color: var(--muted);
white-space: nowrap;
}
.est-05__st i {
inline-size: .4rem;
block-size: .4rem;
border-radius: 50%;
background: var(--faint);
}
.est-05__st--draft i {
background: #b4813f;
}
.est-05__pfoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.est-05__pinfo {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .72rem;
letter-spacing: .02em;
color: var(--faint);
}
.est-05__pager {
display: flex;
gap: .35rem;
}
.est-05__pbtn {
inline-size: 2.2rem;
block-size: 2.2rem;
display: grid;
place-items: center;
cursor: pointer;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 8px;
}
.est-05__pbtn svg {
inline-size: 1rem;
block-size: 1rem;
}
.est-05__pbtn:disabled {
opacity: .45;
cursor: not-allowed;
}
.est-05__pri:focus-visible,
.est-05__ghost:focus-visible,
.est-05__segb:focus-visible,
.est-05__pbtn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.est-05 *,
.est-05 *::before,
.est-05 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-05__tablewrap,
.est-05__glyph,
.est-05__pri {
border-color: CanvasText;
}
}@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-05 {
--bg: #fbfbfc;
--surface: #ffffff;
--sunk: #f4f5f7;
--line: #e6e8ec;
--line-2: #d5d8de;
--ink: #14161b;
--muted: #5a5f6a;
--faint: #8b909a;
--accent: #1f3d7a;
--ok: #2c6a4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-05 *,
.est-05 *::before,
.est-05 *::after {
box-sizing: border-box;
}
.est-05 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-05 {
--bg: oklch(0.988 0.002 265);
--surface: oklch(1 0 0);
--sunk: oklch(0.968 0.004 265);
--line: oklch(0.923 0.006 265);
--line-2: oklch(0.872 0.008 265);
--ink: oklch(0.175 0.01 265);
--muted: oklch(0.465 0.012 265);
--faint: oklch(0.63 0.013 265);
--accent: oklch(0.38 0.095 262);
--ok: oklch(0.48 0.08 155);
}
}
.est-05__page {
inline-size: min(76rem,100%);
justify-self: center;
padding: clamp(1.25rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.15rem;
align-content: start;
}
.est-05__phead {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
}
.est-05__crumbs {
display: flex;
gap: .4rem;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--faint);
}
.est-05__h1 {
margin: .35rem 0 0;
font-size: clamp(1.5rem,3.4vw,2.1rem);
font-weight: 600;
letter-spacing: -.035em;
line-height: 1;
}
.est-05__phacts {
display: flex;
gap: .5rem;
}
.est-05__pri {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 9px;
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--accent) 80%, transparent);
transition: translate .16s, background .16s;
}
.est-05__pri:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-05__ghost {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-05__ghost:hover {
background: var(--sunk);
}
.est-05__pri--sm,
.est-05__ghost--sm {
min-block-size: 2.4rem;
padding: .5rem .85rem;
font-size: .82rem;
}
.est-05__toolbar {
display: flex;
flex-wrap: wrap;
gap: .6rem 1rem;
align-items: center;
justify-content: space-between;
}
.est-05__seg {
display: flex;
padding: .2rem;
background: var(--sunk);
border: 1px solid var(--line);
border-radius: 10px;
}
.est-05__segb {
padding: .42rem .75rem;
font: inherit;
font-size: .82rem;
font-weight: 500;
cursor: pointer;
color: var(--muted);
background: transparent;
border: 0;
border-radius: 7px;
transition: color .16s, background .16s;
}
.est-05__segb:hover {
color: var(--ink);
}
.est-05__segb--on {
color: var(--ink);
background: var(--surface);
box-shadow: 0 1px 2px rgba(20,22,27,.08);
}
.est-05__search {
display: flex;
align-items: center;
gap: .5rem;
flex: 1 1 16rem;
max-inline-size: 22rem;
padding: .5rem .7rem;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
}
.est-05__search svg {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
flex: none;
}
.est-05__search input {
flex: 1 1 auto;
min-inline-size: 0;
font: inherit;
font-size: .85rem;
color: var(--ink);
background: none;
border: 0;
outline: none;
}
.est-05__search:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 12%, transparent);
}
.est-05__tablewrap {
overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 1px 2px rgba(20,22,27,.04);
}
.est-05__table {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
min-inline-size: 44rem;
}
.est-05__table th {
position: sticky;
inset-block-start: 0;
z-index: 1;
text-align: start;
padding: .7rem 1rem;
background: var(--sunk);
border-block-end: 1px solid var(--line);
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .68rem;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--faint);
white-space: nowrap;
}
.est-05__thr {
text-align: end;
}
.est-05__thnum {
inline-size: 8rem;
}
.est-05__emptyrow td {
position: relative;
padding: 0;
}
.est-05__ghosts {
position: absolute;
inset: 0;
display: grid;
grid-auto-rows: 3.25rem;
overflow: hidden;
mask-image: linear-gradient(180deg,#000,transparent 85%);
}
.est-05__ghosts span {
border-block-end: 1px solid var(--line);
position: relative;
}
.est-05__ghosts span::before {
content: '';
position: absolute;
inset-block-start: 1.35rem;
inset-inline-start: 1rem;
block-size: .55rem;
inline-size: min(38%,14rem);
border-radius: 3px;
background: linear-gradient(90deg,var(--sunk),color-mix(in oklab,var(--line) 70%,transparent),var(--sunk));
background-size: 200% 100%;
animation: est-05-shim 4.5s linear infinite;
}
.est-05__ghosts span:nth-child(even)::before {
inline-size: min(26%,10rem);
animation-delay: .6s;
}
@keyframes est-05-shim {
to {
background-position: -200% 0;
}
}
.est-05__empty {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
text-align: center;
gap: .4rem;
padding: clamp(2.5rem,9vh,5rem) 1.25rem clamp(2rem,7vh,4rem);
animation: est-05-in .5s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-05-in {
from {
opacity: 0;
translate: 0 8px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-05__glyph {
inline-size: 3.5rem;
block-size: 3.5rem;
display: grid;
place-items: center;
margin-block-end: .55rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 7%, var(--surface));
border: 1px solid color-mix(in oklab, var(--accent) 18%, var(--line));
border-radius: 12px;
}
.est-05__glyph svg {
inline-size: 1.5rem;
block-size: 1.5rem;
}
.est-05__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
}
.est-05__ep {
margin: 0;
max-inline-size: 40ch;
font-size: .88rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-05__eacts {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
margin-block-start: .85rem;
}
.est-05__row td {
padding: .85rem 1rem;
border-block-end: 1px solid var(--line);
font-size: .87rem;
vertical-align: middle;
}
.est-05__row:last-child td {
border-block-end: 0;
}
.est-05__row:hover td {
background: var(--sunk);
}
.est-05__num {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
color: var(--muted);
}
.est-05__amt {
text-align: end;
font-variant-numeric: tabular-nums;
}
.est-05__st {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .2rem .5rem;
font-size: .74rem;
border-radius: 6px;
background: var(--sunk);
border: 1px solid var(--line);
color: var(--muted);
white-space: nowrap;
}
.est-05__st i {
inline-size: .4rem;
block-size: .4rem;
border-radius: 50%;
background: var(--faint);
}
.est-05__st--draft i {
background: #b4813f;
}
.est-05__pfoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.est-05__pinfo {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .72rem;
letter-spacing: .02em;
color: var(--faint);
}
.est-05__pager {
display: flex;
gap: .35rem;
}
.est-05__pbtn {
inline-size: 2.2rem;
block-size: 2.2rem;
display: grid;
place-items: center;
cursor: pointer;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 8px;
}
.est-05__pbtn svg {
inline-size: 1rem;
block-size: 1rem;
}
.est-05__pbtn:disabled {
opacity: .45;
cursor: not-allowed;
}
.est-05__pri:focus-visible,
.est-05__ghost:focus-visible,
.est-05__segb:focus-visible,
.est-05__pbtn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.est-05 *,
.est-05 *::before,
.est-05 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-05__tablewrap,
.est-05__glyph,
.est-05__pri {
border-color: CanvasText;
}
}(function(){
var root = document.querySelector('.est-05'); if(!root) return;
var body = root.querySelector('#est-05-body'), info = root.querySelector('#est-05-info');
var emptyRow = body.querySelector('.est-05__emptyrow');
var seq = 0;
var clients = [['Meridian Studio','2 400.00'],['Harbour Foods','980.00'],['Ottoline Press','1 150.00']];
function addRow(){
var c = clients[seq % clients.length]; seq++;
var tr = document.createElement('tr');
tr.className = 'est-05__row';
tr.innerHTML = '<td class="est-05__num">INV-20' + (26 + seq) + '-' + (104 + seq) + '</td>' +
'<td>' + c[0] + '</td><td>Today</td><td>In 30 days</td>' +
'<td class="est-05__amt">' + c[1] + '</td>' +
'<td><span class="est-05__st est-05__st--draft"><i></i>Draft</span></td>';
emptyRow.hidden = true;
body.appendChild(tr);
info.textContent = seq + (seq === 1 ? ' draft row' : ' draft rows');
}
root.querySelectorAll('#est-05-new, #est-05-new2').forEach(function(b){ b.addEventListener('click', addRow); });
})();(function(){
var root = document.querySelector('.est-05'); if(!root) return;
var body = root.querySelector('#est-05-body'), info = root.querySelector('#est-05-info');
var emptyRow = body.querySelector('.est-05__emptyrow');
var seq = 0;
var clients = [['Meridian Studio','2 400.00'],['Harbour Foods','980.00'],['Ottoline Press','1 150.00']];
function addRow(){
var c = clients[seq % clients.length]; seq++;
var tr = document.createElement('tr');
tr.className = 'est-05__row';
tr.innerHTML = '<td class="est-05__num">INV-20' + (26 + seq) + '-' + (104 + seq) + '</td>' +
'<td>' + c[0] + '</td><td>Today</td><td>In 30 days</td>' +
'<td class="est-05__amt">' + c[1] + '</td>' +
'<td><span class="est-05__st est-05__st--draft"><i></i>Draft</span></td>';
emptyRow.hidden = true;
body.appendChild(tr);
info.textContent = seq + (seq === 1 ? ' draft row' : ' draft rows');
}
root.querySelectorAll('#est-05-new, #est-05-new2').forEach(function(b){ b.addEventListener('click', addRow); });
})();Here's a working CSS Empty State 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: Empty Data Table / No Rows State
Source: https://codefronts.com/snippets/css-empty-states/empty-data-table-no-rows-state/
The empty table done properly: real column headers stay, a single colspan cell carries the message, and ghost rows fade in behind it so the grid still reads as a table rather than a hole. New invoice adds a genuine draft row and updates the footer count, so the demo covers the transition out of empty as well as the state itself.
## HTML
```html
<div class="est-05">
<div class="est-05__page">
<header class="est-05__phead">
<div class="est-05__ptitle">
<span class="est-05__crumbs">Billing<span aria-hidden="true">/</span>Invoices</span>
<h1 class="est-05__h1">Invoices</h1>
</div>
<div class="est-05__phacts">
<button class="est-05__ghost" type="button">Export</button>
<button class="est-05__pri" type="button" id="est-05-new">New invoice</button>
</div>
</header>
<div class="est-05__toolbar">
<div class="est-05__seg" role="group" aria-label="Status">
<button class="est-05__segb est-05__segb--on" type="button">Unpaid</button>
<button class="est-05__segb" type="button">Paid</button>
<button class="est-05__segb" type="button">Draft</button>
<button class="est-05__segb" type="button">Void</button>
</div>
<label class="est-05__search">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.2" stroke="currentColor" stroke-width="1.7"/><path d="m15.8 15.8 3.6 3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<input type="search" placeholder="Client, number or amount" aria-label="Search invoices">
</label>
</div>
<div class="est-05__tablewrap">
<table class="est-05__table">
<thead>
<tr>
<th scope="col" class="est-05__thnum">Number</th>
<th scope="col">Client</th>
<th scope="col">Issued</th>
<th scope="col">Due</th>
<th scope="col" class="est-05__thr">Amount</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="est-05-body">
<tr class="est-05__emptyrow">
<td colspan="6">
<div class="est-05__ghosts" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="est-05__empty">
<span class="est-05__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="M6 3.5h9l3.5 3.5v13.5H6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14.6 3.8V7.4h3.6M9 12h6M9 15.5h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<p class="est-05__eh">Nothing unpaid</p>
<p class="est-05__ep">Every invoice issued this quarter has cleared. Drafts stay here until you send them.</p>
<div class="est-05__eacts">
<button class="est-05__pri est-05__pri--sm" type="button" id="est-05-new2">New invoice</button>
<button class="est-05__ghost est-05__ghost--sm" type="button">Import from CSV</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="est-05__pfoot">
<span class="est-05__pinfo" id="est-05-info">No rows</span>
<div class="est-05__pager">
<button class="est-05__pbtn" type="button" disabled aria-label="Previous page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m14 7-5 5 5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="est-05__pbtn" type="button" disabled aria-label="Next page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m10 7 5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</footer>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-05 {
--bg: #fbfbfc;
--surface: #ffffff;
--sunk: #f4f5f7;
--line: #e6e8ec;
--line-2: #d5d8de;
--ink: #14161b;
--muted: #5a5f6a;
--faint: #8b909a;
--accent: #1f3d7a;
--ok: #2c6a4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-05 *,
.est-05 *::before,
.est-05 *::after {
box-sizing: border-box;
}
.est-05 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-05 {
--bg: oklch(0.988 0.002 265);
--surface: oklch(1 0 0);
--sunk: oklch(0.968 0.004 265);
--line: oklch(0.923 0.006 265);
--line-2: oklch(0.872 0.008 265);
--ink: oklch(0.175 0.01 265);
--muted: oklch(0.465 0.012 265);
--faint: oklch(0.63 0.013 265);
--accent: oklch(0.38 0.095 262);
--ok: oklch(0.48 0.08 155);
}
}
.est-05__page {
inline-size: min(76rem,100%);
justify-self: center;
padding: clamp(1.25rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.15rem;
align-content: start;
}
.est-05__phead {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
}
.est-05__crumbs {
display: flex;
gap: .4rem;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--faint);
}
.est-05__h1 {
margin: .35rem 0 0;
font-size: clamp(1.5rem,3.4vw,2.1rem);
font-weight: 600;
letter-spacing: -.035em;
line-height: 1;
}
.est-05__phacts {
display: flex;
gap: .5rem;
}
.est-05__pri {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 9px;
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--accent) 80%, transparent);
transition: translate .16s, background .16s;
}
.est-05__pri:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-05__ghost {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-05__ghost:hover {
background: var(--sunk);
}
.est-05__pri--sm,
.est-05__ghost--sm {
min-block-size: 2.4rem;
padding: .5rem .85rem;
font-size: .82rem;
}
.est-05__toolbar {
display: flex;
flex-wrap: wrap;
gap: .6rem 1rem;
align-items: center;
justify-content: space-between;
}
.est-05__seg {
display: flex;
padding: .2rem;
background: var(--sunk);
border: 1px solid var(--line);
border-radius: 10px;
}
.est-05__segb {
padding: .42rem .75rem;
font: inherit;
font-size: .82rem;
font-weight: 500;
cursor: pointer;
color: var(--muted);
background: transparent;
border: 0;
border-radius: 7px;
transition: color .16s, background .16s;
}
.est-05__segb:hover {
color: var(--ink);
}
.est-05__segb--on {
color: var(--ink);
background: var(--surface);
box-shadow: 0 1px 2px rgba(20,22,27,.08);
}
.est-05__search {
display: flex;
align-items: center;
gap: .5rem;
flex: 1 1 16rem;
max-inline-size: 22rem;
padding: .5rem .7rem;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
}
.est-05__search svg {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
flex: none;
}
.est-05__search input {
flex: 1 1 auto;
min-inline-size: 0;
font: inherit;
font-size: .85rem;
color: var(--ink);
background: none;
border: 0;
outline: none;
}
.est-05__search:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 12%, transparent);
}
.est-05__tablewrap {
overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 1px 2px rgba(20,22,27,.04);
}
.est-05__table {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
min-inline-size: 44rem;
}
.est-05__table th {
position: sticky;
inset-block-start: 0;
z-index: 1;
text-align: start;
padding: .7rem 1rem;
background: var(--sunk);
border-block-end: 1px solid var(--line);
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .68rem;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--faint);
white-space: nowrap;
}
.est-05__thr {
text-align: end;
}
.est-05__thnum {
inline-size: 8rem;
}
.est-05__emptyrow td {
position: relative;
padding: 0;
}
.est-05__ghosts {
position: absolute;
inset: 0;
display: grid;
grid-auto-rows: 3.25rem;
overflow: hidden;
mask-image: linear-gradient(180deg,#000,transparent 85%);
}
.est-05__ghosts span {
border-block-end: 1px solid var(--line);
position: relative;
}
.est-05__ghosts span::before {
content: '';
position: absolute;
inset-block-start: 1.35rem;
inset-inline-start: 1rem;
block-size: .55rem;
inline-size: min(38%,14rem);
border-radius: 3px;
background: linear-gradient(90deg,var(--sunk),color-mix(in oklab,var(--line) 70%,transparent),var(--sunk));
background-size: 200% 100%;
animation: est-05-shim 4.5s linear infinite;
}
.est-05__ghosts span:nth-child(even)::before {
inline-size: min(26%,10rem);
animation-delay: .6s;
}
@keyframes est-05-shim {
to {
background-position: -200% 0;
}
}
.est-05__empty {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
text-align: center;
gap: .4rem;
padding: clamp(2.5rem,9vh,5rem) 1.25rem clamp(2rem,7vh,4rem);
animation: est-05-in .5s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-05-in {
from {
opacity: 0;
translate: 0 8px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-05__glyph {
inline-size: 3.5rem;
block-size: 3.5rem;
display: grid;
place-items: center;
margin-block-end: .55rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 7%, var(--surface));
border: 1px solid color-mix(in oklab, var(--accent) 18%, var(--line));
border-radius: 12px;
}
.est-05__glyph svg {
inline-size: 1.5rem;
block-size: 1.5rem;
}
.est-05__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
}
.est-05__ep {
margin: 0;
max-inline-size: 40ch;
font-size: .88rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-05__eacts {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
margin-block-start: .85rem;
}
.est-05__row td {
padding: .85rem 1rem;
border-block-end: 1px solid var(--line);
font-size: .87rem;
vertical-align: middle;
}
.est-05__row:last-child td {
border-block-end: 0;
}
.est-05__row:hover td {
background: var(--sunk);
}
.est-05__num {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
color: var(--muted);
}
.est-05__amt {
text-align: end;
font-variant-numeric: tabular-nums;
}
.est-05__st {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .2rem .5rem;
font-size: .74rem;
border-radius: 6px;
background: var(--sunk);
border: 1px solid var(--line);
color: var(--muted);
white-space: nowrap;
}
.est-05__st i {
inline-size: .4rem;
block-size: .4rem;
border-radius: 50%;
background: var(--faint);
}
.est-05__st--draft i {
background: #b4813f;
}
.est-05__pfoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.est-05__pinfo {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .72rem;
letter-spacing: .02em;
color: var(--faint);
}
.est-05__pager {
display: flex;
gap: .35rem;
}
.est-05__pbtn {
inline-size: 2.2rem;
block-size: 2.2rem;
display: grid;
place-items: center;
cursor: pointer;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 8px;
}
.est-05__pbtn svg {
inline-size: 1rem;
block-size: 1rem;
}
.est-05__pbtn:disabled {
opacity: .45;
cursor: not-allowed;
}
.est-05__pri:focus-visible,
.est-05__ghost:focus-visible,
.est-05__segb:focus-visible,
.est-05__pbtn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.est-05 *,
.est-05 *::before,
.est-05 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-05__tablewrap,
.est-05__glyph,
.est-05__pri {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-05'); if(!root) return;
var body = root.querySelector('#est-05-body'), info = root.querySelector('#est-05-info');
var emptyRow = body.querySelector('.est-05__emptyrow');
var seq = 0;
var clients = [['Meridian Studio','2 400.00'],['Harbour Foods','980.00'],['Ottoline Press','1 150.00']];
function addRow(){
var c = clients[seq % clients.length]; seq++;
var tr = document.createElement('tr');
tr.className = 'est-05__row';
tr.innerHTML = '<td class="est-05__num">INV-20' + (26 + seq) + '-' + (104 + seq) + '</td>' +
'<td>' + c[0] + '</td><td>Today</td><td>In 30 days</td>' +
'<td class="est-05__amt">' + c[1] + '</td>' +
'<td><span class="est-05__st est-05__st--draft"><i></i>Draft</span></td>';
emptyRow.hidden = true;
body.appendChild(tr);
info.textContent = seq + (seq === 1 ? ' draft row' : ' draft rows');
}
root.querySelectorAll('#est-05-new, #est-05-new2').forEach(function(b){ b.addEventListener('click', addRow); });
})();
```Here's a working CSS Empty State 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: Empty Data Table / No Rows State
Source: https://codefronts.com/snippets/css-empty-states/empty-data-table-no-rows-state/
The empty table done properly: real column headers stay, a single colspan cell carries the message, and ghost rows fade in behind it so the grid still reads as a table rather than a hole. New invoice adds a genuine draft row and updates the footer count, so the demo covers the transition out of empty as well as the state itself.
## HTML
```html
<div class="est-05">
<div class="est-05__page">
<header class="est-05__phead">
<div class="est-05__ptitle">
<span class="est-05__crumbs">Billing<span aria-hidden="true">/</span>Invoices</span>
<h1 class="est-05__h1">Invoices</h1>
</div>
<div class="est-05__phacts">
<button class="est-05__ghost" type="button">Export</button>
<button class="est-05__pri" type="button" id="est-05-new">New invoice</button>
</div>
</header>
<div class="est-05__toolbar">
<div class="est-05__seg" role="group" aria-label="Status">
<button class="est-05__segb est-05__segb--on" type="button">Unpaid</button>
<button class="est-05__segb" type="button">Paid</button>
<button class="est-05__segb" type="button">Draft</button>
<button class="est-05__segb" type="button">Void</button>
</div>
<label class="est-05__search">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.2" stroke="currentColor" stroke-width="1.7"/><path d="m15.8 15.8 3.6 3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<input type="search" placeholder="Client, number or amount" aria-label="Search invoices">
</label>
</div>
<div class="est-05__tablewrap">
<table class="est-05__table">
<thead>
<tr>
<th scope="col" class="est-05__thnum">Number</th>
<th scope="col">Client</th>
<th scope="col">Issued</th>
<th scope="col">Due</th>
<th scope="col" class="est-05__thr">Amount</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="est-05-body">
<tr class="est-05__emptyrow">
<td colspan="6">
<div class="est-05__ghosts" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="est-05__empty">
<span class="est-05__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="M6 3.5h9l3.5 3.5v13.5H6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14.6 3.8V7.4h3.6M9 12h6M9 15.5h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<p class="est-05__eh">Nothing unpaid</p>
<p class="est-05__ep">Every invoice issued this quarter has cleared. Drafts stay here until you send them.</p>
<div class="est-05__eacts">
<button class="est-05__pri est-05__pri--sm" type="button" id="est-05-new2">New invoice</button>
<button class="est-05__ghost est-05__ghost--sm" type="button">Import from CSV</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<footer class="est-05__pfoot">
<span class="est-05__pinfo" id="est-05-info">No rows</span>
<div class="est-05__pager">
<button class="est-05__pbtn" type="button" disabled aria-label="Previous page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m14 7-5 5 5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="est-05__pbtn" type="button" disabled aria-label="Next page">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><path d="m10 7 5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</footer>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-05 {
--bg: #fbfbfc;
--surface: #ffffff;
--sunk: #f4f5f7;
--line: #e6e8ec;
--line-2: #d5d8de;
--ink: #14161b;
--muted: #5a5f6a;
--faint: #8b909a;
--accent: #1f3d7a;
--ok: #2c6a4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-05 *,
.est-05 *::before,
.est-05 *::after {
box-sizing: border-box;
}
.est-05 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-05 {
--bg: oklch(0.988 0.002 265);
--surface: oklch(1 0 0);
--sunk: oklch(0.968 0.004 265);
--line: oklch(0.923 0.006 265);
--line-2: oklch(0.872 0.008 265);
--ink: oklch(0.175 0.01 265);
--muted: oklch(0.465 0.012 265);
--faint: oklch(0.63 0.013 265);
--accent: oklch(0.38 0.095 262);
--ok: oklch(0.48 0.08 155);
}
}
.est-05__page {
inline-size: min(76rem,100%);
justify-self: center;
padding: clamp(1.25rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.15rem;
align-content: start;
}
.est-05__phead {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
}
.est-05__crumbs {
display: flex;
gap: .4rem;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--faint);
}
.est-05__h1 {
margin: .35rem 0 0;
font-size: clamp(1.5rem,3.4vw,2.1rem);
font-weight: 600;
letter-spacing: -.035em;
line-height: 1;
}
.est-05__phacts {
display: flex;
gap: .5rem;
}
.est-05__pri {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 9px;
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--accent) 80%, transparent);
transition: translate .16s, background .16s;
}
.est-05__pri:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-05__ghost {
min-block-size: 2.6rem;
padding: .6rem 1rem;
font: inherit;
font-size: .86rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-05__ghost:hover {
background: var(--sunk);
}
.est-05__pri--sm,
.est-05__ghost--sm {
min-block-size: 2.4rem;
padding: .5rem .85rem;
font-size: .82rem;
}
.est-05__toolbar {
display: flex;
flex-wrap: wrap;
gap: .6rem 1rem;
align-items: center;
justify-content: space-between;
}
.est-05__seg {
display: flex;
padding: .2rem;
background: var(--sunk);
border: 1px solid var(--line);
border-radius: 10px;
}
.est-05__segb {
padding: .42rem .75rem;
font: inherit;
font-size: .82rem;
font-weight: 500;
cursor: pointer;
color: var(--muted);
background: transparent;
border: 0;
border-radius: 7px;
transition: color .16s, background .16s;
}
.est-05__segb:hover {
color: var(--ink);
}
.est-05__segb--on {
color: var(--ink);
background: var(--surface);
box-shadow: 0 1px 2px rgba(20,22,27,.08);
}
.est-05__search {
display: flex;
align-items: center;
gap: .5rem;
flex: 1 1 16rem;
max-inline-size: 22rem;
padding: .5rem .7rem;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
}
.est-05__search svg {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
flex: none;
}
.est-05__search input {
flex: 1 1 auto;
min-inline-size: 0;
font: inherit;
font-size: .85rem;
color: var(--ink);
background: none;
border: 0;
outline: none;
}
.est-05__search:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 12%, transparent);
}
.est-05__tablewrap {
overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 1px 2px rgba(20,22,27,.04);
}
.est-05__table {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
min-inline-size: 44rem;
}
.est-05__table th {
position: sticky;
inset-block-start: 0;
z-index: 1;
text-align: start;
padding: .7rem 1rem;
background: var(--sunk);
border-block-end: 1px solid var(--line);
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .68rem;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--faint);
white-space: nowrap;
}
.est-05__thr {
text-align: end;
}
.est-05__thnum {
inline-size: 8rem;
}
.est-05__emptyrow td {
position: relative;
padding: 0;
}
.est-05__ghosts {
position: absolute;
inset: 0;
display: grid;
grid-auto-rows: 3.25rem;
overflow: hidden;
mask-image: linear-gradient(180deg,#000,transparent 85%);
}
.est-05__ghosts span {
border-block-end: 1px solid var(--line);
position: relative;
}
.est-05__ghosts span::before {
content: '';
position: absolute;
inset-block-start: 1.35rem;
inset-inline-start: 1rem;
block-size: .55rem;
inline-size: min(38%,14rem);
border-radius: 3px;
background: linear-gradient(90deg,var(--sunk),color-mix(in oklab,var(--line) 70%,transparent),var(--sunk));
background-size: 200% 100%;
animation: est-05-shim 4.5s linear infinite;
}
.est-05__ghosts span:nth-child(even)::before {
inline-size: min(26%,10rem);
animation-delay: .6s;
}
@keyframes est-05-shim {
to {
background-position: -200% 0;
}
}
.est-05__empty {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
text-align: center;
gap: .4rem;
padding: clamp(2.5rem,9vh,5rem) 1.25rem clamp(2rem,7vh,4rem);
animation: est-05-in .5s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-05-in {
from {
opacity: 0;
translate: 0 8px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-05__glyph {
inline-size: 3.5rem;
block-size: 3.5rem;
display: grid;
place-items: center;
margin-block-end: .55rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 7%, var(--surface));
border: 1px solid color-mix(in oklab, var(--accent) 18%, var(--line));
border-radius: 12px;
}
.est-05__glyph svg {
inline-size: 1.5rem;
block-size: 1.5rem;
}
.est-05__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
}
.est-05__ep {
margin: 0;
max-inline-size: 40ch;
font-size: .88rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-05__eacts {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
margin-block-start: .85rem;
}
.est-05__row td {
padding: .85rem 1rem;
border-block-end: 1px solid var(--line);
font-size: .87rem;
vertical-align: middle;
}
.est-05__row:last-child td {
border-block-end: 0;
}
.est-05__row:hover td {
background: var(--sunk);
}
.est-05__num {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
color: var(--muted);
}
.est-05__amt {
text-align: end;
font-variant-numeric: tabular-nums;
}
.est-05__st {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .2rem .5rem;
font-size: .74rem;
border-radius: 6px;
background: var(--sunk);
border: 1px solid var(--line);
color: var(--muted);
white-space: nowrap;
}
.est-05__st i {
inline-size: .4rem;
block-size: .4rem;
border-radius: 50%;
background: var(--faint);
}
.est-05__st--draft i {
background: #b4813f;
}
.est-05__pfoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.est-05__pinfo {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .72rem;
letter-spacing: .02em;
color: var(--faint);
}
.est-05__pager {
display: flex;
gap: .35rem;
}
.est-05__pbtn {
inline-size: 2.2rem;
block-size: 2.2rem;
display: grid;
place-items: center;
cursor: pointer;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 8px;
}
.est-05__pbtn svg {
inline-size: 1rem;
block-size: 1rem;
}
.est-05__pbtn:disabled {
opacity: .45;
cursor: not-allowed;
}
.est-05__pri:focus-visible,
.est-05__ghost:focus-visible,
.est-05__segb:focus-visible,
.est-05__pbtn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.est-05 *,
.est-05 *::before,
.est-05 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-05__tablewrap,
.est-05__glyph,
.est-05__pri {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-05'); if(!root) return;
var body = root.querySelector('#est-05-body'), info = root.querySelector('#est-05-info');
var emptyRow = body.querySelector('.est-05__emptyrow');
var seq = 0;
var clients = [['Meridian Studio','2 400.00'],['Harbour Foods','980.00'],['Ottoline Press','1 150.00']];
function addRow(){
var c = clients[seq % clients.length]; seq++;
var tr = document.createElement('tr');
tr.className = 'est-05__row';
tr.innerHTML = '<td class="est-05__num">INV-20' + (26 + seq) + '-' + (104 + seq) + '</td>' +
'<td>' + c[0] + '</td><td>Today</td><td>In 30 days</td>' +
'<td class="est-05__amt">' + c[1] + '</td>' +
'<td><span class="est-05__st est-05__st--draft"><i></i>Draft</span></td>';
emptyRow.hidden = true;
body.appendChild(tr);
info.textContent = seq + (seq === 1 ? ' draft row' : ' draft rows');
}
root.querySelectorAll('#est-05-new, #est-05-new2').forEach(function(b){ b.addEventListener('click', addRow); });
})();
```How this works
Keep the table a table. Replacing <tbody> with a div destroys the column widths and the header association. One spanning cell keeps both:
<tr class="est-05__emptyrow">
<td colspan="6"> <!-- ghosts + message live here --> </td>
</tr>Ghost rows sit behind, not instead. They are decorative, absolutely positioned inside the spanning cell, and masked so they never compete with the copy:
.est-05__ghosts{
position:absolute; inset:0;
grid-auto-rows:3.25rem;
mask-image:linear-gradient(180deg,#000,transparent 85%);
}The shimmer is one moving gradient. Cheaper than animating a box, and it stays put when the table scrolls:
.est-05__ghosts span::before{
background:linear-gradient(90deg,var(--sunk),color-mix(in oklab,var(--line) 70%,transparent),var(--sunk));
background-size:200% 100%;
animation:est-05-shim 4.5s linear infinite;
}
@keyframes est-05-shim{ to{ background-position:-200% 0 } }Leaving empty. The first inserted row hides the spanning row and the footer count follows, so the surface never shows an empty message above real data:
emptyRow.hidden = true;
body.appendChild(tr);
info.textContent = seq + (seq === 1 ? ' draft row' : ' draft rows');Make it yours
- Match the
colspanto your column count — it is the one number you must keep in sync with the header - Swap the ghost rows for a static tint if your table is dense and the shimmer competes with real content
- Add an import path next to the primary action; CSV is what finance teams actually reach for
- Change the empty copy per filter: nothing unpaid and no drafts are different messages
- Keep the toolbar interactive while empty so a mistaken filter can be cleared without a reload
- For server-side pagination, leave the pager disabled rather than removed so the layout is stable
Gotchas — read before shipping
- Never remove the header row with the data. Users lose the shape of the table and any sort state
- A
colspanthat no longer matches the header count silently misaligns the whole table - Absolutely positioned children need a positioned ancestor: the
<td>must beposition: relative - Sticky
<th>only works if the scroll container is an ancestor of the table, not the table itself - Disabled pager buttons still need a visible focus ring and an accessible name, or keyboard users lose their place
- Do not animate row insertion for more than a handful of rows; a hundred staggered animations feels like lag
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
position: sticky on <th> needs Chrome 56 / Safari 13 / Firefox 59. mask-image wants the -webkit- prefix below Safari 15.4 — without it the ghost rows end squarely, which is still acceptable. color-mix() and oklch() are behind @supports with hex fallbacks.