M
MetricUI

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.

Revenue by Region
<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.

Revenue by Region
<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>

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.

Revenue by Region
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.

Revenue by Region (Live)
<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 (tooltipHint={true})
Custom hint string
{/* Default hint */}
<BarChart drillDown tooltipHint />

{/* Custom hint text */}
<BarChart drillDown tooltipHint="Click to see breakdown" />

Props

DrillDown.Root

PropTypeDefaultDescription
childrenReactNode(required)Dashboard content. Charts and components that trigger drills.
maxDepthnumber4Maximum nesting depth. New drills at max depth replace the last level.
renderContent(trigger: DrillDownTrigger) => ReactNode | nullReactive render function called on every render. Return null to use stored content.

Chart drillDown props

PropTypeDefaultDescription
drillDowntrue | (event) => ReactNodetrue for auto-table, or a function returning custom drill content.
drillDownMode"slide-over" | "modal""slide-over"Presentation mode for the drill panel.
tooltipHintboolean | stringtrue = "Click to drill down", or pass a custom string.

DrillDownTrigger

PropTypeDefaultDescription
titlestring(required)Display title for the panel header.
fieldstringField name (e.g., 'country', 'plan').
valuestring | numberThe clicked value. Shown as subtitle.
mode"slide-over" | "modal""slide-over"Presentation mode for this specific drill.

useDrillDown() return value

PropertyTypeDescription
isOpenbooleanWhether any drill level is open.
depthnumberCurrent stack depth (0 = closed).
breadcrumbsDrillDownTrigger[]The full breadcrumb trail of open levels.
open(trigger, content) => voidPush a new drill level onto the stack.
back() => voidPop the top drill level (go back).
close() => voidClose all drill levels.
goTo(depth: number) => voidNavigate to a specific breadcrumb depth.
activeContentDrillDownContent | nullCurrent level's content (ReactNode or render function).
activeTriggerDrillDownTrigger | nullCurrent 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.