Empty, Loading, and Error States
The table has three “no rows” states: it has zero rows to display (empty), it’s loading rows that haven’t arrived yet (loading), or a load failed (error). Each state has its own slot component, and the shadcn wrapper ships sensible defaults for all of them — override a slot when the default copy doesn’t fit your domain.
Empty placeholder
Section titled “Empty placeholder”EmptyPlaceholder renders when the model has zero rows. The component receives the table context prop.
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const model = localModel<{ name: string }>({ data: [] })
function EmptyState({ context }: { context: { query: string } }) {
return <div className="p-6 text-sm text-muted-foreground">No products match "{context.query}".</div>
}
export default function App() {
return (
<DataTable EmptyPlaceholder={EmptyState} className="rounded-xl" context={{ query: 'archived' }} model={model} style={{ height: 240 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
)
}Remote loading and error state
Section titled “Remote loading and error state”remoteModel() drives three slot components:
LoadingPlaceholder—initialphase (first unresolved dataset)LoadingOverlay—refreshphase (request params changed, old rows still visible)LoadingFooter—endphase (append mode is fetching more rows below)
Offset viewport fetches show up as placeholder rows, not a table-level loading slot. DataTableLoadingState exposes a start segment for custom integrations, but the built-in remoteModel() never sets it.
Replace any slot through components:
function LoadingOverlay({ loadingState }: LoadingComponentProps) {
if (loadingState.refresh.status === 'error') {
return <div role="status">Refresh failed: {loadingState.refresh.errorMessage}</div>
}
return <div role="status">Refreshing rows...</div>
}
;<DataTable components={{ LoadingOverlay }} model={model} />The shadcn wrapper ships defaults for all three slots. Override them for different copy, skeletons, or error recovery controls.
External UI reads the same state through useVirtuosoLoadingState() (inside the table) or loadingState$ with remote engine hooks (outside).
Error recovery
Section titled “Error recovery”Errors attach to the phase that failed, so a first-load failure can be handled differently from a refresh failure:
function LoadingPlaceholder({ loadingState }: LoadingComponentProps) {
if (loadingState.initial.status === 'error') {
return (
<div className="p-6 text-sm" role="status">
Could not load products. Check your connection and try again.
</div>
)
}
return <div className="p-6 text-sm text-muted-foreground">Loading products...</div>
}
function LoadingFooter({ loadingState }: LoadingComponentProps) {
if (loadingState.end.status === 'error') {
return <div role="status">Could not load more rows.</div>
}
return loadingState.end.status === 'loading' ? <div>Loading more...</div> : null
}
;<DataTable components={{ LoadingFooter, LoadingPlaceholder }} model={model} />Retry buttons belong with the slot that’s showing the error: initial failure → retry from the placeholder, refresh failure → retry from the overlay, append failure → retry from the footer.