UIDex

Empty State

الحالة الفارغة

<Empty> / noRowsOverlay

also calledblank state، zero state، no results state، no data placeholder، first-run screen، null state، إمبتي ستيت، شاشة فارغة، حالة لا توجد نتائج، الحالة الصفرية، رسالة لا توجد بيانات، شاشة البداية الفارغة

An empty state is what a surface shows when the request finished successfully and there is nothing to render: a table with no rows, an inbox with no mail, a search with no matches. It is not a loading state. A skeleton says "the data is on its way", while an empty state says "the answer arrived, and the answer is zero", and confusing the two leaves people waiting for something that will never land. A 404 is something else: it says the address points at nothing, whereas here the address is right and the container exists but holds nothing. Nor is it an error, because an error means we do not know whether the data exists at all. The question that settles the entire design is "why is this empty?", and it has three answers that must never be merged: the user has not created anything yet, a filter or a query ruled everything out, or the request failed, and that last one borrows the same slot without being an empty state, so it gets its own wording and a "Try again" button. Each answer needs its own headline and its own button, and shipping all three as one generic "No data" wastes the most teachable moment in the product.

If you called it…

"the nothing here yet screen""the little drawing when the list is empty""the no results found message after a search""the page with the big add your first thing button""the text that replaces the table when there are no rows"

Live specimen

Interact with the demo. Every part is real and numbered.

الفواتير

لا نتائج مطابقة

لا توجد فاتورة تطابق «INV-2025» في هذه الفترة. جرّب كلمة أقصر أو امسح البحث.

لماذا فرغت؟
RTLنصّ البحث معزول بـ bdi، والزرّ الأساسي عند النهاية المنطقية

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build one empty-state component that takes a variant of "first-use", "no-results", or "error", and never collapses them into a single generic message. Derive the variant from three separate inputs (isLoading, error, items.length) in that order: while loading render a skeleton, not an empty state; on failure render the error variant with a "Try again" button and never the words "no data"; on zero items check whether a query or filter is active. If it is, the variant is no-results, its primary action is "Clear filters", and its supporting line echoes the raw query inside a <bdi>. If not, the variant is first-use and its primary action creates the first item. Keep the container mounted in the DOM with role="status" and swap only its contents. Compose it from an aria-hidden illustration no taller than 120px, an <h2> written in the user's words, one supporting line, and an action row holding exactly one primary action and at most one secondary escape. Never ship a state with no control at all. Use logical properties throughout: margin-inline, padding-inline, inset-inline-start, max-inline-size; never left, right, or margin-left. Lay the action row out with flex and gap, put the primary last in the markup, and do not use row-reverse in RTL. Key any illustration containing words off the locale, and scope every scaleX(-1) to an .illus-directional class. Inside a table, render it in a <td colspan> that spans all columns.

How it behaves right-to-left

This section is ours alone.

Only directional artwork mirrors

mirrors

An illustration that carries direction (an arrow pointing at a toolbar button, a hand dragging a file, a path travelling with the reading order) mirrors with the page, because its meaning is tied to that order. An illustration of an object does not: flipping an empty box, a folder, a magnifier or a mug adds nothing, and it reverses the light source and the shadow along with the picture. Never write a blanket [dir="rtl"] svg { transform: scaleX(-1) }; scope the flip to an explicit class such as .illus-directional that you put on directional assets by hand.

An illustration with Latin text is replaced, not flipped

never mirrors

Flipping an image that contains words renders the letters mirror-backwards, and even counter-flipping the <text> node with its own scaleX(-1) leaves the word sitting in the wrong place in the composition. The same holds for an illustration depicting a miniature interface such as a fake table or inbox: that little UI has to be redrawn in Arabic reading order, not reversed as one block. Key the asset off the locale with { ar: empty-ar.svg, en: empty-en.svg }[locale], and treat it as translatable copy in your language files rather than a fixed asset in the code.

The centred block and the echoed query

never mirrors

The centred block itself needs no mirroring: text-align: center with margin-inline: auto and max-inline-size behaves identically in both directions. The trap is the supporting line when it quotes a Latin query inside an Arabic sentence, because the bidi algorithm pushes the closing quotation mark or the full stop to the wrong end of «لا نتائج لـ INV-2025». Wrap anything the user typed in <bdi>, which is the equivalent of unicode-bidi: isolate plus the HTML dir="auto" attribute, since the CSS direction property only takes ltr or rtl and has no auto، and do not reach for <span dir="ltr">, which forces one direction on a string that may well be Arabic next time.

The action row

mirrors

A flex container's inline axis follows direction, so DOM order alone decides visual order and mirrors for free: put the primary button last in the markup and it lands at the logical end, far left in Arabic and far right in English. Do not add flex-direction: row-reverse or an order property under [dir="rtl"]; that double-flips and drops the primary back on the wrong side. Space the pair with gap instead of margin-left, and replace any margin-left: auto with margin-inline-start: auto.

The empty state inside a table

mirrors

In an HTML table the column order follows the table's direction: the first column sits at the far right under dir="rtl". So never anchor the empty state to a fixed column index: put it in a single <tr> whose <td colspan={columns.length}> spans the full width, and it stays centred after the flip. Keep the <thead> visible above it so the user can tell they are still inside the same table, and do not centre the message with position: absolute plus left or right offsets.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="status"The polite live region that announces "no results" after a filter change, provided it was mounted before the change.
HTML<td colspan="…">The only way to centre the message inside a table without breaking the column grid, and it survives the column order flipping.
Material UIslots={{ noRowsOverlay, noResultsOverlay }}The DataGrid splits "no rows at all" from "the filter matched nothing" into two different slots, the exact split most hand-rolled versions skip.
Ant Design<Empty description /> + locale.emptyTextShips a default illustration plus a compact Empty.PRESENTED_IMAGE_SIMPLE, and lets a table override its empty text through locale.
Chakra UI<EmptyState.Root>Compound parts: Indicator for the art, Title, Description, and Content for the actions, a split that forces you to write all four.
SwiftUIContentUnavailableView.search(text:)The platform itself acknowledges the difference: a general initialiser for the empty state and a dedicated one for zero search results.
CSS:emptyTempting as a one-line fix, but ::after content cannot hold a button and is not dependable in the accessibility tree.

See also

Updated · 2026-08-19