Dashboard with drill-down
A department overview table that combines expandable rows,conditional row styles for health status, and custom cellrenderers for inline progress bars. Expand any row to drill into a card grid of individual team members — the detail panel is a plain React component, not another table.
Department dashboard
Expand a department row to see individual team member utilization.
Expand any row to see team member utilization.
import React, { useState } from 'react';
import DataTable, {
type ConditionalStyles,
type ExpanderComponentProps,
type TableColumn,
} from 'react-data-table-component';
interface TeamMember {
name: string;
role: string;
tickets: number;
utilization: number;
}
interface Department {
id: number;
name: string;
headcount: number;
budget: number;
spent: number;
openTickets: number;
health: 'good' | 'at-risk' | 'critical';
members: TeamMember[];
}
const departments: Department[] = [
{
id: 1,
name: 'Engineering',
headcount: 12,
budget: 1800000,
spent: 1240000,
openTickets: 8,
health: 'good',
members: [
{ name: 'Aria Chen', role: 'Engineering Lead', tickets: 3, utilization: 82 },
{ name: 'Sam Rivera', role: 'DevOps Engineer', tickets: 2, utilization: 91 },
{ name: 'Taylor Kim', role: 'Frontend Eng', tickets: 2, utilization: 74 },
{ name: 'Casey Morgan', role: 'Backend Eng', tickets: 1, utilization: 68 },
],
},
{
id: 2,
name: 'Product',
headcount: 5,
budget: 750000,
spent: 680000,
openTickets: 14,
health: 'at-risk',
members: [
{ name: 'Marcus Webb', role: 'Product Manager', tickets: 7, utilization: 98 },
{ name: 'Dana Park', role: 'Product Designer', tickets: 7, utilization: 95 },
],
},
{
id: 3,
name: 'Design',
headcount: 4,
budget: 480000,
spent: 195000,
openTickets: 3,
health: 'good',
members: [
{ name: 'Priya Kapoor', role: 'Senior Designer', tickets: 2, utilization: 65 },
{ name: 'Alex Kim', role: 'UX Researcher', tickets: 1, utilization: 55 },
],
},
{
id: 4,
name: 'Analytics',
headcount: 3,
budget: 420000,
spent: 410000,
openTickets: 21,
health: 'critical',
members: [
{ name: 'Jordan Ellis', role: 'Data Scientist', tickets: 12, utilization: 100 },
{ name: 'Quinn Adams', role: 'Data Engineer', tickets: 9, utilization: 100 },
],
},
{
id: 5,
name: 'Sales',
headcount: 6,
budget: 600000,
spent: 320000,
openTickets: 5,
health: 'good',
members: [
{ name: 'Taylor Brooks', role: 'Account Manager', tickets: 3, utilization: 72 },
{ name: 'Riley Stone', role: 'Sales Rep', tickets: 2, utilization: 60 },
],
},
];
const HEALTH_CLASS = {
good: 'bg-green-50 text-green-700 border border-green-200',
'at-risk': 'bg-yellow-50 text-yellow-700 border border-yellow-200',
critical: 'bg-red-50 text-red-700 border border-red-200',
};
function UtilBar({ pct }: { pct: number }) {
const color = pct >= 95 ? '#ef4444' : pct >= 80 ? '#f59e0b' : '#22c55e';
return (
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div style={{ width: `${pct}%`, background: color }} className="h-full rounded-full" />
</div>
<span className="text-xs text-gray-500 w-8 text-right">{pct}%</span>
</div>
);
}
function SpendBar({ budget, spent }: { budget: number; spent: number }) {
const pct = Math.min(100, Math.round((spent / budget) * 100));
const color = pct >= 95 ? '#ef4444' : pct >= 80 ? '#f59e0b' : '#6366f1';
return (
<div className="flex items-center gap-2 min-w-[120px]">
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div style={{ width: `${pct}%`, background: color }} className="h-full rounded-full" />
</div>
<span className="text-xs text-gray-500 w-8 text-right">{pct}%</span>
</div>
);
}
function initials(name: string) {
return name
.split(' ')
.map(part => part[0])
.join('')
.slice(0, 2)
.toUpperCase();
}
function DepartmentDetail({ data: dept }: ExpanderComponentProps<Department>) {
return (
<div className="px-8 py-4 bg-gray-50 border-b border-gray-100">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">Team members</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{dept.members.map(member => (
<div
key={member.name}
className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg px-3 py-2.5"
>
<div className="w-9 h-9 rounded-full bg-brand-100 text-brand-700 text-xs font-semibold flex items-center justify-center shrink-0">
{initials(member.name)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-gray-900 truncate">{member.name}</p>
<span className="text-[11px] text-gray-400 shrink-0">{member.tickets} open</span>
</div>
<p className="text-xs text-gray-500 truncate mb-1">{member.role}</p>
<UtilBar pct={member.utilization} />
</div>
</div>
))}
</div>
</div>
);
}
const conditionalRowStyles: ConditionalStyles<Department>[] = [
{ when: r => r.health === 'critical', style: { backgroundColor: '#fff7f7' } },
{ when: r => r.health === 'at-risk', style: { backgroundColor: '#fffdf0' } },
];
const columns: TableColumn<Department>[] = [
{
id: 'name',
name: 'Department',
selector: r => r.name,
sortable: true,
grow: 1,
},
{
id: 'headcount',
name: 'Headcount',
selector: r => r.headcount,
sortable: true,
right: true,
width: '110px',
},
{
id: 'spend',
name: 'Budget spend',
selector: r => r.spent,
sortable: true,
width: '200px',
cell: r => (
<div className="w-full">
<div className="text-xs text-gray-500 mb-1">
${r.spent.toLocaleString()} / ${r.budget.toLocaleString()}
</div>
<SpendBar budget={r.budget} spent={r.spent} />
</div>
),
},
{
id: 'openTickets',
name: 'Open tickets',
selector: r => r.openTickets,
sortable: true,
right: true,
width: '120px',
},
{
id: 'health',
name: 'Health',
selector: r => r.health,
sortable: true,
width: '110px',
cell: r => <span className={`text-xs px-2 py-0.5 rounded-full ${HEALTH_CLASS[r.health]}`}>{r.health}</span>,
},
];
export default function DashboardDrilldownDemo() {
const [allExpanded, setAllExpanded] = useState(false);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400">Expand any row to see team member utilization.</p>
<button
onClick={() => setAllExpanded(v => !v)}
className="px-2.5 py-1 text-xs font-medium rounded-md border border-gray-200 text-gray-600 hover:border-gray-300"
>
{allExpanded ? 'Collapse all' : 'Expand all'}
</button>
</div>
<div className="rounded-xl border border-gray-200 overflow-hidden">
<DataTable
columns={columns}
data={departments}
expandableRows
expandableRowsComponent={DepartmentDetail}
expandableRowExpanded={() => allExpanded}
conditionalRowStyles={conditionalRowStyles}
defaultSortFieldId="name"
highlightOnHover
/>
</div>
</div>
);
}This demo uses Tailwind utility classes for layout — swap in your own CSS when copying into your project.
Conditional row styles
Tint rows based on a derived health field so problems are visible at a glance — no extra column needed:
const conditionalRowStyles: ConditionalStyles<Department>[] = [
{ when: r => r.health === 'critical', style: { backgroundColor: '#fff7f7' } },
{ when: r => r.health === 'at-risk', style: { backgroundColor: '#fffdf0' } },
];A card layout in the expander, not a nested table
expandableRowsComponent can render anything — it doesn't have to be anotherDataTable. A grid of team-member cards reads better here than a table-in-a-table. The parent row arrives as data:
function DepartmentDetail({ data: dept }: ExpanderComponentProps<Department>) {
return (
<div style={{ padding: '12px 32px', background: '#f9fafb' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{dept.members.map(member => (
<div key={member.name} style={{ display: 'flex', gap: 12, background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: '10px 12px' }}>
<div className="avatar">{initials(member.name)}</div>
<div style={{ flex: 1 }}>
<strong>{member.name}</strong> — {member.role}
<UtilBar pct={member.utilization} />
</div>
</div>
))}
</div>
</div>
);
}
<DataTable
columns={columns}
data={departments}
expandableRows
expandableRowsComponent={DepartmentDetail}
conditionalRowStyles={conditionalRowStyles}
/>Expand all / collapse all
expandableRowExpanded re-syncs whenever its result changes, so a single boolean in state — flipped by the "Expand all" button above the demo — expands or collapses every department at once:
const [allExpanded, setAllExpanded] = useState(false);
<button onClick={() => setAllExpanded(v => !v)}>
{allExpanded ? 'Collapse all' : 'Expand all'}
</button>
<DataTable
columns={columns}
data={departments}
expandableRows
expandableRowsComponent={DepartmentDetail}
expandableRowExpanded={() => allExpanded}
/>Inline progress bars in cells
Use a cell renderer for columns that need richer content than a plain value. The renderer receives the full row, so you can derive bar width and color from the data:
{
name: 'Budget spend',
cell: r => {
const pct = Math.round((r.spent / r.budget) * 100);
const color = pct >= 95 ? '#ef4444' : pct >= 80 ? '#f59e0b' : '#6366f1';
return (
<div style={{ width: '100%' }}>
<div style={{ fontSize: 11, color: '#6b7280', marginBottom: 3 }}>
${r.spent.toLocaleString()} / ${r.budget.toLocaleString()}
</div>
<div style={{ height: 4, background: '#f3f4f6', borderRadius: 2, overflow: 'hidden' }}>
<div style={{ width: `${pct}%`, background: color, height: '100%' }} />
</div>
</div>
);
},
}