20 CSS Empty States

6 Pure CSS14 Light JSPublished

20 hand-coded empty states — the screens most teams ship last and worst. An empty state is what your app shows when there is nothing to show, and it is the moment a user decides whether the product is broken or just new. Each one here names what happened and gives you somewhere to go next. Scoped under .est-NN, prefers-reduced-motion guarded, framework-agnostic.

Related30 CSS Notifications22 CSS Skeleton Loaders20 CSS Form Validation20 CSS Banners & Alerts20 Tailwind CSS Empty States

Premium icon pickPartner: Nucleo

Affiliate link

Need icons for the components you just copied?

You just browsed 20 empty states. Nucleo is a library of 44,000+ SVG icons in five families (Core, UI, Sharp, Pixel and Micro Bold), with web, Mac and Windows apps to manage them. The All Icons Bundle is $149 with lifetime updates.

Get the Nucleo bundle

Nucleo. We may earn a commission, at no extra cost to you.

01. No Search Results State (opens in a new tab)

Published

Light JS
A search that returns nothing inside a real issue tracker: filter chips above the results, the query echoed back in monospace, and one primary recovery that removes the filter most likely to be the problem. Dropping a chip swaps the empty state for real rows, so the demo shows both halves of the state machine rather than a poster of the sad half.

02. First-Time Onboarding State (opens in a new tab)

Published

Light JS
First run for a team workspace, where the empty state is the onboarding: three real setup steps, a progress ring in the sidebar that tracks them, and a template rail for people who would rather copy than configure. Ticking a step rewrites the heading and the body copy, so the surface earns its place instead of repeating itself .

03. Empty Shopping Cart State (opens in a new tab)

Published

Light JS
An empty bag that behaves like a shop rather than a dead end: the bag panel on the left, three pieces the store actually wants to move on the right, and adding one turns the empty state into a real line item with a subtotal and a checkout. Warm paper, an Instrument Serif headline, a dotted letterpress texture and a slowly rotating dashed seal — boutique retail rather than SaaS.

04. Inbox Zero / All Caught Up State (opens in a new tab)

Published

Light JS
Inbox zero in a three-pane mail client, where both panes have to say something: the list shows the cleared state with the time of the last archive, and the reading pane stays quiet rather than shouting a second celebration. Undo last archive brings the thread back into both panes at once, which is the only reason a user visits this screen at all.

05. Empty Data Table / No Rows State (opens in a new tab)

Published

Light JS
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.

06. Filtered List No Matches State (opens in a new tab)

Published

Pure CSS
Filtering with zero JavaScript: three facet checkboxes over a five-person hiring pipeline, and an inline empty card that appears only for the combination that genuinely returns nothing — senior plus design — driven entirely by chained :has() on the form root.
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

07. Empty Dashboard / No Analytics Data (opens in a new tab)

Published

Light JS
A dashboard with no data yet, treated as a wiring problem rather than a void: the chart grid is drawn but desaturated behind a focused connect panel, and picking a source brings the whole grid to life — bars rise, the donut fills, the cohort cells warm up, the sidebar status flips to syncing. No invented numbers anywhere, just axis scaffolding until something real arrives.

08. 500 Server Error Page Layout (opens in a new tab)

Published

Light JS
A 500 page that behaves like an incident view: a gradient-clipped error code, a plain-language explanation of what did and did not happen to the request, a reference the user can quote, and a recovery log that appends real lines as you retry. The replay succeeds, the button becomes a completed state and the headline rewrites itself .

09. Empty Wishlist / Favorites State (opens in a new tab)

Published

Pure CSS
Three saved-state tabs in a boutique storefront, each with its own empty message, driven entirely by CSS state: radios plus :has() switch the panels, ghost product frames sit behind the first card so the grid still reads as a grid, and a row of recently viewed pieces gives the page somewhere to go.

10. Empty Notification Center Flyout (opens in a new tab)

Published

Pure CSS
A navbar bell that opens a real notification flyout with zero JavaScript: a single checkbox drives the open state and :has() lifts it to the panel, the bell, and the page behind it, which blurs and dims as the flyout takes focus. Three radio-driven tabs each carry their own empty state.

11. Failed to Load / Connection Error State (opens in a new tab)

Published

Light JS
Not a whole-page error — one panel on a working dashboard that failed to load, which is what actually happens in production. The failed card keeps its header, carries a compact diagnostic row (endpoint, code, attempt) and a retry that walks through loading and into a real chart, while the two panels beside it stay live and untouched so the user can see the blast radius is small.

12. Offline / No Internet Connection State (opens in a new tab)

Published

Light JS
Offline as a state with inventory: the queue of unsynced edits is on screen, each row named and timestamped, so the user knows exactly what is at risk. Check connection reconnects, the sidebar light and the orb turn green, every row flips to synced in a short stagger, and the copy changes from a warning to a receipt. The reassurance is the list, not the sentence.

13. Permission Denied / Restricted Access State (opens in a new tab)

Published

Light JS
A 403 that shows the door it is refusing: the document renders behind a blur and a radial scrim, so the user can see the file exists without reading a word of it, and the gate on top names the owners who can approve access. Requesting access swaps the buttons for a confirmation and rewrites the explanation — the screen resolves rather than repeating itself.

14. 404 Page Not Found Layout (opens in a new tab)

Published

Light JS
A 404 for a publication rather than an app: an oversized serif numeral, an asymmetric two-column composition with a rule down the middle, and a working archive search that filters a real index as you type — including its own nested empty result. The right column carries the most-read list, because on an editorial site the best recovery from a dead link is another good story.
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

15. Deleted or Archived Items State (opens in a new tab)

Published

Light JS
An empty trash that answers the only question users have here: how long do I have? The retention policy is on screen in the sidebar and in the copy, and the interactive control is a retention picker that rewrites both at once — 7, 30 or 90 days. The bin itself is drawn in CSS, with a lid that lifts once every six seconds.

16. Skeleton Loading to Empty State Transition (opens in a new tab)

Published

Light JS
The transition nobody tests: skeleton out, empty state in, without the page jumping. The skeleton reserves the exact block size the empty state will occupy — one min-block-size on the slot — so the swap costs zero layout shift, and the shimmer is a single moving background-position rather than an animated box.

17. Accessible Empty State (ARIA Live) (opens in a new tab)

Published

Light JS
The accessible version of a no-results state, in a library catalogue: a visually hidden role="status" region announces the outcome, focus moves to the results container so a screen reader lands on the message rather than hunting for it, and the recovery suggestions are real buttons that re-run the search. Nothing about the accessibility work is visible — which is the point.

18. Pure CSS Illustrated Empty State (opens in a new tab)

Published

Pure CSS
An illustrated empty state with no image files and no SVG: a copper pot, its broth, two handles, a plate shadow, three curls of steam and a herb sprig, all built from elements, gradients, border-radius and clip-path. It sits in a real cookbook app — header, collections rail, saved tab .

19. Dark Mode Compatible Empty State (opens in a new tab)

Published

Pure CSS
One empty state that survives three appearance modes in pure CSS: a three-way System / Light / Dark control built from radios, read by :has(), that repaints an entire settings surface by swapping nine tokens. System defers to prefers-color-scheme, so the same markup follows the OS until the user overrides it.

20. Inline / Micro Empty State Component (opens in a new tab)

Published

Pure CSS
The empty state for small places, shown where it actually happens: a sidebar widget, a dashboard tile and a table cell, all rendering the same component inside one product page. It responds to its container, not the viewport — drag the right edge of the first tile and the illustration goes, then the body copy, then the layout flattens.
FAQ

Frequently asked questions

What makes a good empty state?
Three things, and most production empty states manage one. First, say what happened in terms the user recognises — "No results for merino socks" beats "No data", because it confirms the system heard the request rather than implying something broke. Second, offer the next action, and make it the obvious one: clear the filters, create the first project, continue shopping. An empty state with no action is a dead end, and the user's only remaining move is the back button. Third, match the tone to the cause. An empty inbox is an achievement and can be quietly celebratory; a failed network request is a problem and needs a retry button, not a friendly illustration. The common failure is treating all three cases identically — a cheerful mascot on a permission-denied screen reads as the interface making light of something the user cannot fix.
How do I make an empty state accessible?
The core requirement is that the change is announced, not just rendered. When a list empties because a filter matched nothing, a sighted user sees the swap instantly while a screen reader user hears nothing at all unless you tell it to speak. Wrap the count or status in a live region — role="status", or aria-live="polite" — and write the new text into it when the state changes. Two details make the difference between working and not: the live region must already exist in the DOM before you write to it, because injecting the region and its content together announces nothing; and it must be polite rather than assertive for a routine result count, since assertive interrupts whatever the user is currently hearing. Keep the announcement short and factual — "No results for merino socks" — rather than reading out the whole empty state including the illustration alt text and the button label.
How do I show an empty state inside a data table?
Put a single row in the <tbody> with one cell spanning every column, using colspan set to the column count. That keeps the table structurally valid, which matters more than it looks: a <div> dropped inside a <tbody> is invalid HTML, and browsers will hoist it out of the table entirely, so your carefully centred empty state ends up rendering above or below the table rather than inside it. Centring the content is then a matter of giving that one cell some vertical padding and a text-align. Two things worth getting right: keep the table header visible so the user can still see what columns exist and what they were filtering on, and if the emptiness is caused by a filter rather than by genuinely having no data, say so and offer to clear it — those are different situations and deserve different copy.
Should an empty state use an illustration?
Sometimes, and rarely the way stock illustration packs suggest. An illustration earns its place when it makes the state instantly recognisable — an empty cart, an empty inbox — and it costs its place when it delays the message, pushes the action below the fold, or adds a network request to a screen that is already the result of something not loading. That last point matters more than it sounds: an empty state caused by a failed fetch should not itself depend on fetching a 200KB SVG. Drawing the graphic in CSS avoids that entirely and keeps the whole component in one file. Where an illustration is decorative rather than informative, mark it aria-hidden="true" so screen readers skip straight to the text — otherwise assistive technology announces a description of a picture before it reaches the sentence that actually explains what happened.
How do I transition from a loading skeleton to an empty state without layout shift?
Reserve the space before either one renders. The shift happens because the skeleton and the empty state are different heights, so the moment you swap them everything below jumps — and that jump is a Cumulative Layout Shift event, which Core Web Vitals measures and Google uses. The fix is to give the container a min-height sized to the taller of the two states, so the box is stable and only its contents change. From there the swap can be a cross-fade rather than a hard cut, which reads as calmer: @starting-style with transition-behavior: allow-discrete animates an element that is entering or leaving display: none, which plain transitions cannot do. Both are progressive enhancement — without them the states swap instantly, which is still correct, just less smooth.
What is the difference between an empty state, an error state, and a loading state?
They answer three different questions and should never share one component. A loading state says "this is coming" — the request is in flight and the outcome is unknown, so a skeleton or spinner is honest. An empty state says "this worked and there is nothing here" — the request succeeded and returned zero results, which is a normal outcome and often the user's own doing, like an over-narrow filter. An error state says "this did not work" — something failed, the user is not at fault, and there must be a retry path. Conflating them produces the two most common bugs in this area: showing an empty state while data is still loading, which makes a working app look broken, and showing an empty state after a failed request, which quietly hides the failure and leaves the user with no way to recover.
How do I handle offline and connection-loss states?
Listen for the online and offline events on window and reflect the current value of navigator.onLine in your UI. The important caveat is that navigator.onLine only tells you whether the device has a network interface up — it says nothing about whether your server is reachable, so a captive portal, a DNS failure or a downed API all report as online. Treat it as a hint rather than a truth: use it to show a banner immediately when connectivity drops, but rely on actual request failures to drive the per-component error states. Two things make the pattern feel considered rather than alarming: keep any content you already have on screen rather than replacing it with an offline screen, and queue the user's pending actions so they are not lost, telling them plainly that the actions will send when the connection returns.
How do these port to React, Vue, Svelte and Tailwind?
The CSS ports unchanged — every .est-NN selector and custom property is framework-agnostic, and six of the twenty demos need no JavaScript at all. Where a demo does use script, it drives the trigger rather than the styling: filtering a list to zero, failing a fetch, clearing a cart. In a framework that logic is already yours, so what you take from these demos is the markup and the CSS. The one pattern worth copying carefully is the live region: in React, render the region unconditionally and change only its text content, because conditionally mounting the whole region on empty means the announcement fires as the node is created and screen readers frequently miss it. Vue and Svelte have the same trap. For Tailwind the states map to conditional classes as usual, and the min-height that prevents layout shift between the skeleton and the empty state is worth keeping as an explicit utility rather than leaving to chance.

Related collections

30 CSS Badges preview

30 CSS Badges

30 hand-coded CSS badges for status indicators, notifications, membership tiers, live-data displays, and SEO / DevOps / financial dashboards — upload progress, typing indicator, transit line status, Core Web Vitals, ECG heartbeat, CI/CD build pipeline, countdown ring, live price ticker, keycap shortcut, wax seal, conference lanyard, and holographic collectibles. Copy-paste HTML and CSS, WCAG 2.2 accessible, MIT licensed.

20 CSS Banners & Alerts preview

20 CSS Banners & Alerts

20 hand-coded CSS banner and alert bars for e-commerce storefronts, SaaS dashboards, marketing sites, and compliance-bound products. Covers GDPR cookie consent with a real preferences panel, promo bars with countdown timers, free-shipping threshold progress, app-install smart banners, maintenance and incident status bars, newsletter signup bars, age verification gates, limited-stock urgency banners, live event announcements, browser upgrade notices, geo and currency switchers, sticky top announcement bars, bottom cookie bars, inline form validation alerts, icon-aligned alert banners, left-border accent alerts, diagonal stripe promos, full-width hero banners, animated gradient border alerts, and a text-wrapping laboratory that shows five strategies for the unbreakable string that breaks more banners than anything else. Every bar is scoped under a .ba-NN prefix for no-collision pasting, guards prefers-reduced-motion, carries the correct aria-live and role semantics, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.

25 CSS Blockquotes preview

25 CSS Blockquotes

25 CSS blockquotes for article pull quotes, testimonial cards, docs callout admonitions, statement banners, and social-post embeds. Covers giant decorative quotation marks, minimalist hairline and classic left borders, animated gradient and glowing neon borders, glassmorphism and aurora backgrounds, brutalist cards, speech bubbles, thick accent rails, highlighted mark text, newspaper multi-column layouts, an expandable read-more disclosure, a scroll-snap testimonial slider, and one-click copy.

Search CodeFronts

Loading…