@k2b/ui
Empty states
Compact and panel placeholders plus route-level not-found states.
Empty icons sit in soft chips of the app color; loading, error, and inline icons stay plain. The examples name each non-default layout choice. NotFoundState handles a route-level dead end.
No projects
No projects
Loading projects
Projects unavailable
TSX
Copyimport { Button, NotFoundState, Placeholder } from "@k2b/ui"; <Placeholder surface="paper" title="No projects" description="Create the first project." icon="ti ti-folder-off" action={<Button size="sm">New project</Button>}/><Placeholder surface="paper" variant="panel" icon="ti ti-folder-off" title="No projects" description="Create the first project." /><Placeholder surface="paper" state="loading" variant="panel" title="Loading projects" description="Fetching the latest projects." /><Placeholder surface="paper" state="error" align="left" title="Projects unavailable" description="Reload the page to try again." /><NotFoundState code="404" title="Project not found" description="It may have been moved." action={{ label: "All projects", href: "/projects" }} /><Placeholder surface="paper" variant="inline" align="left" icon="ti ti-file-text" description="No notes yet" />Placeholder explains an empty, filtered, inaccessible, loading, or failed region inside a page.
NotFoundState handles a whole-page dead end and can provide one route back.
The application owns the message and any recovery action.
Use empty states
Use Placeholder for an empty table, loading panel, or section that failed to
load. Use the compact variant inside a section and the panel variant for a
whole work area; the panel variant centers its content in the height it
receives, such as class="flex-1" in a column. Use the inline variant with
align="left" for an empty list or sidebar section: one line with a small icon
where the first row would be.
state controls semantics and the default icon, while variant and align
control layout independently. Name non-default layout choices in examples so
different states are not mistaken for different component implementations.
Use surface="paper" only when the placeholder stands alone in a page area,
such as an empty list that replaces a table. Inside a surface that already
frames it, such as a section, dialog, detail panel, or Paper, leave
surface at its default. One frame per surface: a paper placeholder inside
Paper, SettingsSection, PanelDialog, a prompt dialog, or DetailPanel
keeps its layout but draws no border, background, or shadow of its own.
Use NotFoundState when a requested page or resource cannot be shown. Display
a code such as 404 only when it accurately describes the result.
Use state="empty" for “Nothing here yet”, state="no-results" for “No
matches” after filtering, and state="no-access" for “You do not have access”.
Keep these states distinct so the next step is clear. They draw no box inside
AppWorkspace, even with surface="paper". AppOverview.EmptyState is a plain
panel placeholder without a paper panel.
Every load error offers Try again through state="error" and onRetry.
Preserve the person's selection and filters during recovery. For tables, use
DataTable loading placeholders at the real
row height and keep existing rows while refreshing.
Import
import {
Button,
NotFoundState,
Placeholder,
} from "@k2b/ui";Properties
Placeholder
| Property | Type | Default | Purpose |
|---|---|---|---|
title |
JSX.Element |
none | Names the state. |
description |
JSX.Element |
none | Explains the state or next step. |
icon |
string |
state-dependent | Overrides the Tabler icon class. empty has no default icon; no-results uses search-off and no-access uses lock. |
action |
JSX.Element |
none | Adds a recovery or creation action. |
onRetry |
() => void |
none | Appends a localized Try again button after action for errors. |
state |
"empty" | "no-results" | "no-access" | "loading" | "error" |
"empty" |
Selects state semantics and default icon. |
variant |
"compact" | "inline" | "panel" |
"compact" |
Chooses stacked section spacing, one list row, or a larger work-area treatment. |
surface |
"none" | "paper" |
"none" |
Adds the shared paper surface for a standalone page area. |
align |
"center" | "left" |
"center" |
Aligns the content. |
class |
string |
none | Adds classes to the root element. |
An error state uses role="alert". A loading state uses role="status",
aria-live="polite", and aria-busy="true". The loading icon stops spinning
when reduced motion is requested. Empty, no-results, and no-access icons sit in a soft chip of
the accent ink --k2b-app-workspace-active; loading and error keep their status colors without a chip,
and inline placeholders keep a plain glyph. The icon box keeps the same size
in every state: 2rem for compact, 2.5rem for panel, and 1rem for inline.
Pass state="loading" or state="error" for a loading or failure message
instead of a loader or alert icon on the default empty state; the
placeholder-states repository check reports such a call.
NotFoundState
| Property | Type | Default | Purpose |
|---|---|---|---|
title |
string |
required | States what cannot be shown. |
description |
string |
none | Adds useful context without exposing internal errors. |
code |
string |
none | Displays a large status code. |
icon |
string |
none | Displays an icon when no code is set. |
action |
{ label: string; href: string; icon?: string } |
none | Provides one navigation path out of the dead end. |
NotFoundState renders a div, not a main, so the page shell retains
ownership of landmarks.
Accessibility
Write titles that distinguish an empty result from a failed request. Show an empty state only after a read that succeeded: a failed read shows its error, not a claim that nothing is there. While the same region reloads, keep the empty state in place instead of swapping in an empty frame. Loading text should name what is loading. The built-in recovery button says Try again (Erneut versuchen in German). Empty, no-results, and no-access have no role or live announcement.
When an action fills the region, such as adding the first record, the placeholder and its focused button leave. Move focus to the new content, such as the created row, so it does not fall back to the page.
NotFoundState renders its title as an h1. Do not place another page-level
heading around it.
Runtime
Both components render on the server. An anchor action works without browser code; an island owns any callback-based action.
Example
<Placeholder
surface="paper"
title="No notes yet"
description="Create the first note for this workspace."
action={<Button size="sm" onClick={createNote}>Create note</Button>}
/>
<Placeholder
surface="paper"
state="loading"
variant="panel"
title="Loading notes"
/>
<Placeholder
state="error"
align="left"
title="Notes unavailable"
description="Your selection is kept."
onRetry={reloadNotes}
/>
<Placeholder
variant="inline"
align="left"
icon="ti ti-file-text"
description="No notes yet"
/>
<NotFoundState
code="404"
title="Application not found"
description="The application may have moved or access may have changed."
action={{ label: "Back to applications", href: "/apps" }}
/>