Empty State
Cards shown when a list has no data — on first use before anything is created, after a search returns no results, or when a filtered view is empty.
When to use this recipe​
Every data list page and table needs at least one empty state. Replace the default "No results" text with one of these patterns to guide users toward the next action.
Components used​
Code​
- HTML · @primus/ui-core
- React
- Angular
<!-- First-use empty state -->
<div class="card" style="padding:2.5rem;text-align:center;max-width:320px">
<h3>No tenants yet</h3>
<p style="color:var(--muted-foreground)">
Create your first tenant to get started with the platform.
</p>
<button class="small" style="width:100%">+ Create tenant</button>
</div>
<!-- No search results -->
<div class="card" style="padding:2.5rem;text-align:center;max-width:320px">
<h3>No results found</h3>
<p style="color:var(--muted-foreground)">
No tenants match "acme xyz". Try a different search term.
</p>
<button class="outline small" style="width:100%">Clear search</button>
</div>
<!-- No filter results -->
<div class="card" style="padding:2.5rem;text-align:center;max-width:320px">
<h3>No suspended tenants</h3>
<p style="color:var(--muted-foreground)">
There are no tenants matching the current filters.
</p>
<button class="outline small" style="width:100%">Clear filters</button>
</div>
import { PrimusButton } from 'primus-react-ui';
interface EmptyStateProps {
title: string;
description: string;
action?: { label: string; onClick: () => void };
variant?: 'default' | 'outline';
}
function EmptyState({ title, description, action, variant = 'default' }: EmptyStateProps) {
return (
<div className="card" style={{ padding: '2.5rem', textAlign: 'center', maxWidth: 320 }}>
<h3 style={{ margin: '0 0 0.375rem', fontSize: '1rem', fontWeight: 700 }}>{title}</h3>
<p style={{ margin: '0 0 1.5rem', color: 'var(--muted-foreground)', fontSize: '0.8375rem' }}>
{description}
</p>
{action && (
<PrimusButton variant={variant} style={{ width: '100%' }} onClick={action.onClick}>
{action.label}
</PrimusButton>
)}
</div>
);
}
// Usage
function TenantsPage() {
const { data, loading, query, setQuery, filters, setFilters } = useTenants();
if (loading) return <div aria-busy="true" data-spinner="large" />;
if (data.length === 0 && !query && Object.keys(filters).length === 0) {
return <EmptyState title="No tenants yet"
description="Create your first tenant to get started."
action={{ label: '+ Create tenant', onClick: () => navigate('/tenants/new') }} />;
}
if (data.length === 0 && query) {
return <EmptyState title="No results found"
description={`No tenants match "${query}".`}
action={{ label: 'Clear search', onClick: () => setQuery('') }}
variant="outline" />;
}
return <PrimusDataTable columns={cols} data={data} rowKey="id" />;
}
<!-- tenants-list.component.html -->
<ng-container [ngSwitch]="viewState">
<!-- Loading -->
<div *ngSwitchCase="'loading'" aria-busy="true" data-spinner="large"></div>
<!-- First-use empty -->
<div *ngSwitchCase="'empty-first'"
class="card" style="padding:2.5rem;text-align:center;max-width:320px">
<h3>No tenants yet</h3>
<p style="color:var(--muted-foreground)">Create your first tenant to get started.</p>
<primus-button style="width:100%" (clicked)="openCreate()">+ Create tenant</primus-button>
</div>
<!-- No search results -->
<div *ngSwitchCase="'empty-search'"
class="card" style="padding:2.5rem;text-align:center;max-width:320px">
<h3>No results found</h3>
<p style="color:var(--muted-foreground)">No tenants match "{{ query }}".</p>
<primus-button variant="outline" style="width:100%" (clicked)="clearSearch()">
Clear search
</primus-button>
</div>
<!-- Data table -->
<primus-data-table *ngSwitchDefault
[columns]="columns" [data]="tenants" rowKey="id"
[searchable]="true" [paginated]="true">
</primus-data-table>
</ng-container>