Interaction
DrillDown
A portal-based drill-down system for exploring chart data in detail. Click a bar, slice, or row to open a slide-over panel or modal with detail content — auto-generated or fully custom.
Overview
The DrillDown system is composed of three pieces: a DrillDown.Root wrapper that provides context and renders the overlay portal, a drillDown prop on chart components that wires up click-to-drill, and hooks ( useDrillDown, useDrillDownAction) for programmatic control.
import { DrillDown, useDrillDownAction } from "metricui";
function Dashboard() {
return (
<DrillDown.Root>
<BarChart
data={data}
indexBy="region"
categories={["revenue"]}
drillDown // true = auto-table
tooltipHint // "Click to drill down"
/>
</DrillDown.Root>
);
}Zero-Config DrillDown
Pass drillDown={true} to any chart. Clicking a bar, slice, or point opens a slide-over with an auto-generated detail table showing filtered rows, summary KPIs, and search.
<DrillDown.Root>
<BarChart
data={revenueByRegion}
indexBy="region"
categories={["revenue"]}
title="Revenue by Region"
drillDown
tooltipHint
/>
</DrillDown.Root>Custom Content
Pass a function to drillDown to render custom content. The function receives the click event and returns a ReactNode. Use MetricGrid, KpiCards, DataTable, or any component inside.
<DrillDown.Root>
<BarChart
data={revenueByRegion}
indexBy="region"
categories={["revenue", "accounts"]}
title="Revenue by Region"
drillDown={(event) => {
const regionAccounts = accounts.filter(
(a) => a.region === event.indexValue,
);
return (
<MetricGrid>
<KpiCard title="Accounts" value={regionAccounts.length} />
<KpiCard title="Total Revenue"
value={regionAccounts.reduce((s, a) => s + a.revenue, 0)}
format="currency"
/>
<DataTable
data={regionAccounts}
columns={[
{ key: "account", header: "Account" },
{ key: "plan", header: "Plan" },
{ key: "revenue", header: "Revenue", format: "currency" },
]}
dense
/>
</MetricGrid>
);
}}
tooltipHint="Click to view accounts"
/>
</DrillDown.Root>Modal Mode
Set drillDownMode="modal" to open the drill content in a centered modal instead of the default slide-over panel. The modal is capped at 85vh height and 3xl width.
<DrillDown.Root>
<BarChart
data={revenueByRegion}
indexBy="region"
categories={["revenue"]}
drillDown
drillDownMode="modal"
tooltipHint
/>
</DrillDown.Root>Nested Drills
Drill levels stack up to 4 deep. Inside drill content, use useDrillDownAction() to open sub-drills. Breadcrumbs appear automatically for navigation, and Escape goes back one level.
function NestedDrill() {
const openDrill = useDrillDownAction();
return (
<BarChart
data={revenueByRegion}
indexBy="region"
categories={["revenue"]}
drillDown={(event) => {
const regionAccounts = accounts.filter(
(a) => a.region === event.indexValue,
);
return (
<div className="space-y-4">
{regionAccounts.map((acct) => (
<button
key={acct.account}
onClick={() =>
openDrill(
{ title: acct.account, field: "account", value: acct.account },
<KpiCard title="Revenue" value={acct.revenue} format="currency" />,
)
}
>
{acct.account}
</button>
))}
</div>
);
}}
/>
);
}Reactive Content (renderContent)
DrillDown.Root accepts a renderContent prop — a function called on every render with the active trigger. This is useful when the drill content should react to live data changes (e.g., real-time dashboards). Return null to fall through to the stored content.
<DrillDown.Root
renderContent={(trigger) => {
const region = String(trigger.value);
const regionAccounts = accounts.filter((a) => a.region === region);
const totalRevenue = regionAccounts.reduce((s, a) => s + a.revenue, 0);
return (
<MetricGrid>
<KpiCard title="Region" value={region} />
<KpiCard title="Total Revenue" value={totalRevenue} format="currency" />
<KpiCard title="Accounts" value={regionAccounts.length} format="number" />
</MetricGrid>
);
}}
>
<BarChart
data={liveData}
indexBy="region"
categories={["revenue"]}
drillDown
tooltipHint
/>
</DrillDown.Root>Hooks
Two hooks provide full programmatic control over the drill-down system.
useDrillDownAction()
Returns an openDrill(trigger, content) function. Use it in click handlers, buttons, or any event to imperatively open a drill level.
useDrillDown()
Returns the full drill-down state: { isOpen, depth, breadcrumbs, back, close, goTo, open, activeContent, activeTrigger }. Use it to build custom navigation, conditionally render UI based on drill state, or close drills programmatically.
useDrillDown() state
isOpen: false
depth: 0
breadcrumbs: []
import { useDrillDown, useDrillDownAction } from "metricui";
function MyComponent() {
const drill = useDrillDown();
const openDrill = useDrillDownAction();
return (
<div>
<button onClick={() =>
openDrill(
{ title: "US Details", field: "region", value: "US" },
<KpiCard title="Revenue" value={142000} format="currency" />,
)
}>
Open US drill
</button>
<p>isOpen: {String(drill?.isOpen)}</p>
<p>depth: {drill?.depth}</p>
<p>breadcrumbs: [{drill?.breadcrumbs.map(b => b.title).join(", ")}]</p>
</div>
);
}Tooltip Hints
The tooltipHint prop adds an action hint to chart tooltips so users know they can click to drill. Pass truefor the default "Click to drill down" text, or pass a custom string.
{/* Default hint */}
<BarChart drillDown tooltipHint />
{/* Custom hint text */}
<BarChart drillDown tooltipHint="Click to see breakdown" />Props
DrillDown.Root
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | (required) | Dashboard content. Charts and components that trigger drills. |
maxDepth | number | 4 | Maximum nesting depth. New drills at max depth replace the last level. |
renderContent | (trigger: DrillDownTrigger) => ReactNode | null | — | Reactive render function called on every render. Return null to use stored content. |
Chart drillDown props
| Prop | Type | Default | Description |
|---|---|---|---|
drillDown | true | (event) => ReactNode | — | true for auto-table, or a function returning custom drill content. |
drillDownMode | "slide-over" | "modal" | "slide-over" | Presentation mode for the drill panel. |
tooltipHint | boolean | string | — | true = "Click to drill down", or pass a custom string. |
DrillDownTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | (required) | Display title for the panel header. |
field | string | — | Field name (e.g., 'country', 'plan'). |
value | string | number | — | The clicked value. Shown as subtitle. |
mode | "slide-over" | "modal" | "slide-over" | Presentation mode for this specific drill. |
useDrillDown() return value
| Property | Type | Description |
|---|---|---|
isOpen | boolean | Whether any drill level is open. |
depth | number | Current stack depth (0 = closed). |
breadcrumbs | DrillDownTrigger[] | The full breadcrumb trail of open levels. |
open | (trigger, content) => void | Push a new drill level onto the stack. |
back | () => void | Pop the top drill level (go back). |
close | () => void | Close all drill levels. |
goTo | (depth: number) => void | Navigate to a specific breadcrumb depth. |
activeContent | DrillDownContent | null | Current level's content (ReactNode or render function). |
activeTrigger | DrillDownTrigger | null | Current level's trigger metadata. |
Notes
- DrillDown.Root must wrap any component that uses drillDown props or drill-down hooks.
- The overlay is rendered via a portal on document.body — it works regardless of your layout's overflow or z-index.
- Maximum 4 nested drill levels. At max depth, a new drill replaces the last level.
- Escape key goes back one level. Clicking the backdrop closes all levels.
- Body scroll is locked while a drill is open.
- drillDown={true} auto-generates a detail view using AutoDrillTable — summary KPIs + a searchable DataTable filtered to the clicked value.
- The drillDown prop takes priority over crossFilter for the click action when both are set.
- Content can be a static ReactNode or a render function (() => ReactNode) for reactive/live content.
- renderContent on DrillDown.Root is called on every render — use it for real-time dashboards where drill content should reflect live data.
- useDrillDown() returns null if no DrillDown.Root is present — always null-check in shared components.