55 / Feedback
Empty Row
One line saying a list came back empty, for a table, a list, or a popover.
| Fund | 1Y | Expense |
|---|---|---|
| No funds match these filters. | ||
"use client"
import { DemoVariants } from "@/components/demos/demo-variants"
import { EmptyRow } from "mischief-ui/empty-row"
const columns = ["Fund", "1Y", "Expense"]
export function EmptyRowDemo() {
return (
<DemoVariants
label="Empty row"
variants={[
{
id: "table",
label: "In a table",
render: () => (
<table className="border-border w-full max-w-md border-collapse rounded-[var(--radius)] border text-sm">
<thead>
<tr>
{columns.map((column) => (
<th
key={column}
className="border-border text-muted-foreground border-b px-4 py-2 text-left text-xs font-medium"
scope="col"
>
{column}
</th>
))}
</tr>
</thead>
<tbody>
<EmptyRow colSpan={columns.length}>
No funds match these filters.
</EmptyRow>
</tbody>
</table>
),
},
{
id: "list",
label: "In a list",
render: () => (
<div className="border-border w-full max-w-md rounded-[var(--radius)] border">
<EmptyRow />
</div>
),
},
]}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/empty-rowimport { EmptyRow } from "mischief-ui/empty-row"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
import * as React from "react" import { cn } from "@/lib/utils" export type EmptyRowProps = Omit< React.HTMLAttributes<HTMLElement>, "children"> & { children?: React.ReactNode /** * Renders a table row spanning this many columns. Without it the row is a * paragraph, for a list or a popover. */ colSpan?: numberUsage
export function Results({ rows }) {
if (rows.length === 0) {
return <EmptyRow>No funds match these filters.</EmptyRow>
}
return <FundsTable rows={rows} />
}The smallest of three
Empty Row is a line inside something. Empty State fills a panel with a title, a sentence, and a way forward. Not Found fills a page. They are separate components rather than sizes of one, because a filtered table wants a line and a blank page wants a heading, and a component that tries to be both is wrong at one end.
Reach for this one where the surrounding thing already explains itself: a table under its own heading, a search popover, a filter pane. There is nothing to introduce, only a result to report.
Inside a table
A paragraph is not valid inside a table body, and a row that does not span the columns leaves the message wedged under the first one. Pass colSpan and the component renders the row and the cell for you.
<tbody>
{rows.length === 0 ? (
<EmptyRow colSpan={columns.length}>Nothing filed yet.</EmptyRow>
) : (
rows.map((row) => <Row key={row.id} {...row} />)
)}
</tbody>API
childrenReactNodeThe line itself. Defaults to "No matches."colSpannumberRenders a table row spanning this many columns.classNamestringClasses for the element.Accessibility
A row inside a table is a real table row spanning every column, so the table's shape stays intact and a screen reader reads the message once rather than as a stray cell. Elsewhere it is a paragraph, announced by whatever region already holds the list. It is not a live region: put it in one only if the list can empty while someone is reading it.