Table states
Examples
The table component provides table states functionality using only Jutro components. For advanced use cases or existing integrations, you can optionally leverage external libraries such as TanStack Table to handle parts of data management logic.
You can review two types of code examples for table states that demonstrate using only Jutro components and using TanStack Table for part of data management logic. The examples have all files needed for you to review them in your environment, such as functions simulating asynchronous data fetching. Each example has a different set of files.
Check out the Usage section for details about how to design a table properly, and the different configuration options we provide.
Empty state implementation
To implement an empty state in your table, use the TableEmptyState component within the TableBody when your data array is empty or when a conditional check determines no content is to be displayed.
The TableEmptyState component provides a default ErrorOutlineIcon icon and a Nothing to display label. These can be modified using optional mainMessageContent and icon properties. It also accepts optional properties like detailedMessageContent for additional context and actions for interactive elements.
Use the mainMessageVariant and mainMessageTag properties to control the heading hierarchy and styling.
Configure action buttons through the actions array, where each action object requires a label property and accepts all optional button properties, such as onClick event handler.
import React, { useId, useState } from 'react';
import {
BodyCell,
BodyRow,
Button,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableEmptyState,
TableHeader,
Typography,
} from '@jutro/components';
import type { TableProps } from '@jutro/components';
const mockRowData = {
product: 'Go Commercial Auto',
insured: 'Marshall Reagan',
premium: 'USD 1,236.39',
};
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{
tableId: string;
children?: React.ReactNode;
}> = ({ tableId, children }) => (
<div>
<div>
<Typography variant="heading-2" id={tableId}>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
{children}
</div>
);
export function EmptyStateStory(
args: TableProps
): React.ReactElement {
const tableId = useId();
const [hasData, setHasData] = useState(false);
return (
<div>
<TableTitle tableId={tableId}>
{hasData && (
<div>
<Button
label="Clear data"
onClick={() => setHasData(false)}
/>
</div>
)}
</TableTitle>
<Table
noStripedRows={args.noStripedRows}
className={args.className}
aria-labelledby={tableId}
>
<TableHeader>
<HeaderRow>
<HeaderCell columnIndex={0}>Product</HeaderCell>
<HeaderCell columnIndex={1}>Insured</HeaderCell>
<HeaderCell columnIndex={2}>Premium</HeaderCell>
</HeaderRow>
</TableHeader>
<TableBody>
{!hasData ? (
<TableEmptyState
mainMessageVariant='heading-3'
mainMessageTag='h3'
mainMessageContent='Nothing to display'
detailedMessageContent={
<>
No records found.
<br />
Click <strong>Add a new entry</strong> to create a new entry.
</>
}
actions={[
{
label: 'Add a new entry',
variant: 'tertiary',
onClick: () => {
setHasData(true);
},
},
]}
/>
) : (
<BodyRow>
<BodyCell rowIndex={0} columnIndex={0}>
{mockRowData.product}
</BodyCell>
<BodyCell rowIndex={0} columnIndex={1}>
{mockRowData.insured}
</BodyCell>
<BodyCell rowIndex={0} columnIndex={2}>
{mockRowData.premium}
</BodyCell>
</BodyRow>
)}
</TableBody>
</Table>
</div>
);
}
Loading state implementation
To implement a loading state in your table, use the TableLoader component within the TableBody during data fetch operations or any asynchronous processing that requires user feedback.
The TableLoader component provides a standardized loading experience with a spinner icon and the default "Loading data" label. You can modify both icon and label using available properties. The component automatically centers the content within the table body.
Implement loading states by conditionally rendering the TableLoader component based on your data fetching status. Use Boolean flags like loading or isLoading from your state management to control when the loading state appears. The loading state is to replace the normal table content during data operations and automatically transition to either the populated table, an empty state, or an appropriate error state once the operation completes.
Accessibility
Set aria-busy="true" on the Table component during loading operations to inform assistive technologies that content is being updated.
Use aria-live="polite" on the Table component to create a live region that automatically announces state changes to screen reader users, ensuring they receive timely feedback when content transitions between loading, empty, and populated states without interrupting their current focus or reading flow.
Component implementation
import React, { useId, useState } from 'react';
import {
BodyCell,
BodyRow,
Button,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableEmptyState,
TableHeader,
Typography,
TableLoader,
} from '@jutro/components';
import { simulateAsyncFetch } from './simulateAsyncFetch';
import type { TableProps, TableEmptyStateProps } from '@jutro/components';
const mockRowData = [
{
id: '11111',
product: 'Go Commercial Auto',
insured: 'Marshall Reagan',
premium: 'USD 1,236.39',
},
{
id: '80077',
product: "Go Worker's Compensation",
insured: 'April Kub',
premium: 'USD 173.99',
},
{
id: '64487',
product: 'USA Personal Auto',
insured: 'Abel Rippin',
premium: 'USD 1,228.69',
},
{
id: '12345',
product: 'Go Commercial Auto',
insured: 'John Smith',
premium: 'USD 892.45',
},
];
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{
tableId: string;
children?: React.ReactNode;
}> = ({ tableId, children }) => (
<div>
<div>
<Typography
variant="heading-2"
id={tableId}
>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
{children}
</div>
);
const TableBodyChildren: React.FC<{
hasData: boolean;
setHasData: React.Dispatch<React.SetStateAction<boolean>>;
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
args: TableProps & TableEmptyStateProps;
}> = ({ hasData, setHasData, loading, setLoading, args }) => {
const addData = () => {
setHasData(true);
};
{
}
if (loading) {
return <TableLoader />;
}
{
}
if (!hasData) {
return (
<TableEmptyState
mainMessageVariant="heading-3"
mainMessageTag="h3"
mainMessageContent="Nothing to display"
detailedMessageContent={
<>
Unable to load data.
<br />
Click <strong>Reload</strong> to reload the data.
</>
}
actions={[
{
label: 'Reload',
variant: 'tertiary',
onClick: simulateAsyncFetch(addData, {
setLoading,
}),
},
]}
/>
);
}
return (
<>
{mockRowData.map((row, index) => (
<BodyRow key={row.id}>
<BodyCell
rowIndex={index}
columnIndex={0}
>
{row.product}
</BodyCell>
<BodyCell
rowIndex={index}
columnIndex={1}
>
{row.insured}
</BodyCell>
<BodyCell
rowIndex={index}
columnIndex={2}
>
{row.premium}
</BodyCell>
</BodyRow>
))}
</>
);
};
export function EmptyStateReloadStory(
args: TableProps & TableEmptyStateProps
): React.ReactElement {
const tableId = useId();
const [hasData, setHasData] = useState(false);
const [loading, setLoading] = useState(false);
return (
<div>
<TableTitle tableId={tableId}>
{hasData && (
<div>
<Button
label="Clear data"
onClick={() => setHasData(false)}
/>
</div>
)}
</TableTitle>
<Table
noStripedRows={args.noStripedRows}
className={args.className}
aria-labelledby={tableId}
aria-busy={loading ? 'true' : 'false'}
aria-live="polite"
>
<TableHeader>
<HeaderRow>
<HeaderCell columnIndex={0}>Product</HeaderCell>
<HeaderCell columnIndex={1}>Insured</HeaderCell>
<HeaderCell columnIndex={2}>Premium</HeaderCell>
</HeaderRow>
</TableHeader>
<TableBody>
<TableBodyChildren
hasData={hasData}
setHasData={setHasData}
loading={loading}
setLoading={setLoading}
args={args}
/>
</TableBody>
</Table>
</div>
);
}
Data management
const WAIT_TIME = 400;
type SimulateAsyncFetchOptions = {
setLoading?: (value: boolean) => void;
delay?: number;
};
export function simulateAsyncFetch<T extends unknown[], R>(
callback: (...args: T) => R | Promise<R>,
options?: SimulateAsyncFetchOptions
): (...args: T) => Promise<R> {
let timer: number | null = null;
return async (...args: T): Promise<R> => {
options?.setLoading?.(true);
try {
return await callback(...args);
} finally {
if (timer) {
clearTimeout(timer);
}
timer = window.setTimeout(
() => options?.setLoading?.(false),
options?.delay || WAIT_TIME
);
}
};
}


