` so all children inherit.
#### Variant CSS Variables
| CSS Variable | Purpose | Default value |
|----------------------|---------------------------|----------------------|
| --mu-card-bg | Card background | var(--card-bg) |
| --mu-card-border | Border color | var(--card-border) |
| --mu-card-border-w | Border width | 1px |
| --mu-card-shadow | Box shadow | none |
| --mu-card-radius | Border radius | 1rem |
| --mu-cell-bg | StatGroup cell background | var(--mu-card-bg) |
| --mu-hover-shadow | Hover shadow | (see Hover section) |
| --mu-hover-border | Hover border color | (see Hover section) |
#### Built-in Variants
| Variant | Background | Border | Extra |
|-----------|----------------------------|--------------------------|------------------------------------|
| default | var(--card-bg) | 1px var(--card-border) | clean bordered card |
| outlined | transparent | 2px var(--card-border) | inset shadow, 0.75rem radius |
| ghost | color-mix accent-tinted bg | none (0px) | no border |
| elevated | var(--card-bg) | transparent | multi-layer shadow, 1.25rem radius |
#### Custom Variants
Define CSS variables under a `[data-variant="..."]` selector to create custom variants:
```css
[data-variant="glass"] {
--mu-card-bg: rgba(255,255,255,0.08);
--mu-card-border: rgba(255,255,255,0.15);
--mu-card-border-w: 1px;
--mu-card-shadow: 0 8px 32px rgba(0,0,0,0.1);
--mu-card-radius: 1rem;
backdrop-filter: blur(12px);
}
```
```tsx
{children}
```
### Dense Mode (CSS-Variable-Driven)
Dense mode is implemented via CSS variables set by `[data-dense="true"]` attribute selectors.
MetricProvider renders a wrapper `
` with `data-dense` so all
children inherit. Chart heights and margins in dense mode are centralized via the
`useDenseValues()` hook.
#### Dense CSS Variables
| CSS Variable | Normal | Dense |
|----------------------|-----------|-----------|
| --mu-padding | 1.25rem | 0.625rem |
| --mu-gap | 1rem | 0.5rem |
| --mu-title-size | 0.75rem | 0.625rem |
| --mu-value-size | 1.875rem | 1.25rem |
| --mu-value-size-bare | 1.5rem | 1.125rem |
| --mu-table-py | 0.75rem | 0.375rem |
| --mu-table-px | 1.25rem | 0.75rem |
| Chart height | 300px | 200px |
### Dark Mode Setup
```tsx
import { ThemeProvider } from "metricui/theme";
import { MetricProvider } from "metricui";
export default function Layout({ children }) {
return (
{children}
);
}
```
### Custom Chart Colors
```tsx
{children}
```
### Motion / Animation
```tsx
// Slower, bouncier
// Faster, snappier
// Disable all animation
```
---
## Patterns
### Basic Dashboard
```tsx
import {
MetricProvider,
KpiCard,
StatGroup,
AreaChart,
BarChart,
DataTable,
} from "metricui";
function Dashboard() {
return (
{/* KPI Row */}
{/* Charts */}
{/* Table */}
);
}
```
### KPI Card with All Features
```tsx
console.log("Copied:", v)}
drillDown={{ label: "View breakdown", onClick: () => router.push("/revenue") }}
animate={{ countUp: true, duration: 1200 }}
description="Total revenue from all channels including recurring and one-time."
footnote="Updated 5 minutes ago"
/>
```
### Time Series with Comparison
```tsx
({ x: d.date, y: d.revenue })) },
]}
comparisonData={[
{ id: "Revenue", data: lastMonth.map(d => ({ x: d.date, y: d.revenue })) },
]}
format="currency"
title="Revenue Trend"
subtitle="Current vs Previous Period"
xAxisLabel="Date"
yAxisLabel="Revenue"
referenceLines={[
{ axis: "y", value: targetRevenue, label: "Target", color: "#10B981", style: "dashed" },
]}
thresholds={[
{ from: 0, to: dangerThreshold, color: "#EF4444", opacity: 0.05, label: "Below target" },
]}
/>
```
### Bar Chart Presets
```tsx
{/* Grouped comparison */}
{/* 100% stacked composition */}
{/* Horizontal sorted */}
{/* With targets and negative values */}
```
### Data Fetching States
```tsx
function MetricCard({ data, isLoading, error }) {
return (
refetch(),
} : undefined}
empty={!isLoading && !error && data?.revenue == null ? {
message: "No revenue data for this period",
icon: ,
action: ,
} : undefined}
stale={data?.updatedAt ? {
since: new Date(data.updatedAt),
warningAfter: 30,
} : undefined}
/>
);
}
{/* Grouped state prop */}
{/* Charts and tables have the same props */}
```
### Conditional Formatting
```tsx
{/* Simple threshold coloring */}
{/* Compound conditions */}
{/* Inverted trend (lower is better) */}
```
### Donut Chart
```tsx
{/* Full data format */}
{/* simpleData shorthand */}
{/* Half donut (gauge style) */}
```
### Dual Axis Combo Chart
```tsx
```
### Table with Formatting
```tsx
import { DataTable, Badge } from "metricui";
(
= 0 ? "text-emerald-600" : "text-red-500"}>
{v >= 0 ? "+" : ""}{v.toFixed(1)}%
),
},
{ key: "status", header: "Status",
render: (v) => (
{v}
),
},
{ key: "lastOrder", header: "Last Order", format: "duration" },
]}
pageSize={10}
searchable
striped
stickyHeader
title="Sales Report"
subtitle="Q1 2024"
footer={{
company: "Total",
revenue: "$1,284,500",
growth: "",
status: "",
lastOrder: "",
}}
onRowClick={(row) => router.push(`/companies/${row.id}`)}
/>
```
### Stat Group Row
```tsx
,
},
{
label: "New Users",
value: 1284,
previousValue: 1150,
format: "compact",
icon: ,
},
{
label: "Avg Order Value",
value: 110.8,
previousValue: 105.2,
format: "currency",
icon: ,
},
{
label: "Churn Rate",
value: 2.1,
previousValue: 2.8,
format: "percent",
invertTrend: true,
icon: ,
},
{
label: "NPS Score",
value: 72,
previousValue: 68,
icon: ,
},
]}
columns={5}
variant="elevated"
onStatClick={(stat) => setSelectedMetric(stat.label)}
/>
```
---
## Filter System
MetricUI's filter system is **UI only** — it tells you what the user selected, but does not filter your data. You bring the data; the filter tells you what range to fetch.
### FilterProvider
Context provider that holds the active filter state (period, comparison mode, dimension filters). Wrap your dashboard in this.
```tsx
import { FilterProvider } from "metricui";
{children}
```
#### FilterProvider Props
- `defaultPreset`: PeriodPreset — Initial preset. Default: none (no period selected).
- `defaultComparison`: ComparisonMode — Initial comparison mode. Default: "none".
- `referenceDate`: Date — Anchors preset date calculations to a historical date instead of now. Useful for demos and tests with historical data. E.g., `referenceDate={new Date('2024-12-31')}` makes "30d" calculate from Dec 31 instead of today.
- `children`: React.ReactNode
### useMetricFilters()
Hook to read and update the active filter state from the nearest FilterProvider. Returns null if no FilterProvider is present.
```tsx
import { useMetricFilters } from "metricui";
function MyComponent() {
const filters = useMetricFilters();
// filters.period → { start: Date, end: Date } | null
// filters.preset → "30d" | null
// filters.comparisonMode → "none" | "previous" | "year-over-year"
// filters.comparisonPeriod → { start: Date, end: Date } | null
// filters.dimensions → Record
// filters.setPeriod(range, preset?)
// filters.setPreset(preset)
// filters.setCustomRange(start, end)
// filters.setComparisonMode(mode)
// filters.setDimension(field, values)
// filters.clearDimension(field)
// filters.clearAll()
}
```
### Filter Types
```ts
interface DateRange { start: Date; end: Date; }
type PeriodPreset = "7d" | "14d" | "30d" | "90d" | "month" | "quarter" | "ytd" | "year" | "all";
type ComparisonMode = "previous" | "year-over-year" | "none";
interface FilterState {
period: DateRange | null;
preset: PeriodPreset | null;
comparisonMode: ComparisonMode;
comparisonPeriod: DateRange | null;
dimensions: Record;
}
type FilterContextValue = FilterState & FilterActions;
```
### CrossFilterProvider / useCrossFilter
Cross-filtering captures click selections from charts and tables into a shared context. **Signal only** — it stores what the user clicked. It never touches, filters, or transforms data. You read the selection, filter your own data, pass to charts.
```tsx
import { CrossFilterProvider, useCrossFilter } from "metricui";
function App() {
return (
);
}
function Dashboard() {
const cf = useCrossFilter();
const chartData = useMemo(() => {
if (!cf?.isActive) return allData;
return allData.filter(row => row[cf.selection.field] === cf.selection.value);
}, [cf?.isActive, cf?.selection]);
return (
<>
{/* Source chart: keeps full data so user sees what they clicked */}
{/* Sibling charts: get filtered data */}
>
);
}
```
#### crossFilter prop
Add `crossFilter` to any chart or DataTable to emit selections on click:
- `crossFilter={true}` — uses the `index`/`indexBy` field name
- `crossFilter={{ field: "region" }}` — overrides the field name
- Supported on: BarChart, DonutChart, AreaChart, HeatMap, DataTable
#### useCrossFilter() API
- `selection`: `{ field: string; value: string | number } | null`
- `isActive`: `boolean` — true when something is selected
- `select(sel)`: set or toggle a selection
- `clear()`: clear the current selection
#### useCrossFilteredData (convenience hook)
```tsx
import { useCrossFilteredData } from "metricui";
// Returns filtered data when a cross-filter selection is active, original data otherwise.
const chartData = useCrossFilteredData(allData, "browser");
// When "Chrome" selected: allData.filter(row => row.browser === "Chrome")
// When nothing selected: allData
```
For multi-filter scenarios (e.g., dropdown + cross-filter), use `useCrossFilter()` directly.
#### Cross-Filter Behavior
- Click same value again to deselect (toggle)
- Press Escape to clear from anywhere
- Without a CrossFilterProvider, the crossFilter prop is silently ignored
---
### PeriodSelector
Category: filter
A date-range picker with preset periods, custom ranges, and comparison toggle. Uses forwardRef.
#### PeriodSelector Props
- `presets`: PeriodPreset[] — default: ["7d","30d","90d","month","quarter","ytd"] — Which presets to show in the dropdown.
- `allowCustom`: boolean — default: true — Show custom date-range inputs.
- `comparison`: boolean — default: false — Show comparison toggle button.
- `comparisonOptions`: ComparisonMode[] — default: ["previous","year-over-year"] — Comparison modes to cycle through.
- `onChange`: (period: DateRange, preset: PeriodPreset | null) => void — Standalone callback. Works without FilterProvider.
- `onComparisonChange`: (mode: ComparisonMode, period: DateRange | null) => void — Standalone comparison callback.
- `dense`: boolean — default: false — Compact mode. Falls back to MetricProvider.
- `className`: string — Additional CSS classes.
- `id`: string — HTML id.
- `data-testid`: string — Test id.
#### PeriodSelector Minimal Example
```tsx
```
#### PeriodSelector Examples
```tsx
// With comparison toggle and default preset
```
```tsx
// Standalone mode — no FilterProvider needed
{
fetchData(period.start, period.end);
}}
/>
```
```tsx
// Full dashboard with period filter
{/* calls useMetricFilters() to read period */}
```
#### PeriodSelector Notes
- PeriodSelector is UI only — it tells you what the user selected, but does not filter data.
- Without FilterProvider, use onChange for standalone mode.
- Comparison periods are auto-computed from the active period.
- Dense mode inherits from MetricProvider or can be set per-component.
---
### DropdownFilter
Category: filter
A single or multi-select dropdown for dimension filtering. Search, grouped options, count badges, and FilterContext integration. Uses forwardRef.
#### DropdownFilter Props
- `label`: string (required) — Label shown on the trigger button.
- `options`: DropdownOption[] | string[] (required) — Options to display. Pass string[] as shorthand.
- `value`: string | string[] — Controlled selected value(s).
- `defaultValue`: string | string[] — Default value for uncontrolled mode.
- `onChange`: (value: string | string[]) => void — Change handler. Receives string (single) or string[] (multiple).
- `multiple`: boolean — default: false — Allow multiple selections.
- `searchable`: boolean — default: auto (true when > 8 options) — Show search input inside dropdown.
- `searchPlaceholder`: string — default: "Search..." — Placeholder text for search input.
- `field`: string — FilterContext field name. Reads/writes to dimensions.
- `showAll`: boolean — default: true (in multiple mode) — Show "All" option that clears selection.
- `allLabel`: string — default: "All" — Label for the All option.
- `maxHeight`: number — default: 280 — Max height of dropdown in px.
- `dense`: boolean — default: false — Compact mode. Falls back to MetricProvider.
- `className`: string — Additional CSS classes.
- `classNames`: { root?, trigger?, dropdown?, option?, search? } — Sub-element class overrides.
- `id`: string — HTML id.
- `data-testid`: string — Test id.
#### DropdownOption Type
```ts
interface DropdownOption {
value: string; // Unique value
label?: string; // Display label (defaults to value)
count?: number; // Optional count badge
icon?: ReactNode; // Icon rendered before label
group?: string; // Group this option belongs to
}
```
#### DropdownFilter Minimal Example
```tsx
```
#### DropdownFilter Examples
```tsx
// Multi-select with count badges
```
```tsx
// Searchable with grouped options
```
```tsx
// Connected to FilterContext
{/* Other components read filters.dimensions.region */}
```
#### DropdownFilter Notes
- DropdownFilter is UI only — it captures the selection, not filters data.
- Without FilterProvider, use onChange for standalone mode.
- Search is auto-enabled when there are more than 8 options.
- The "All" option is shown by default in multiple mode. It clears all selections.
- Grouped options render with section headers.
- Dense mode inherits from MetricProvider or can be set per-component.
---
### SegmentToggle
Category: filter
A pill-style toggle for switching between segments. Single/multi-select, icons, badge counts, color-coded segments, FilterContext integration. Uses forwardRef.
#### SegmentToggle Props
- `options`: SegmentOption[] | string[] (required) — Segment options. Pass string[] as shorthand.
- `value`: string | string[] — Controlled active segment(s).
- `defaultValue`: string | string[] — default: first option — Default value for uncontrolled mode.
- `onChange`: (value: string | string[]) => void — Change handler. Receives string (single) or string[] (multiple).
- `multiple`: boolean — default: false — Allow multiple selections.
- `field`: string — FilterContext field name. Reads/writes to dimensions.
- `orientation`: "horizontal" | "vertical" — default: "horizontal" — Layout orientation.
- `size`: "sm" | "md" | "lg" — default: "md" — Size variant.
- `fullWidth`: boolean — default: false — Stretch segments to fill container.
- `dense`: boolean — default: false — Compact mode. Falls back to MetricProvider.
- `className`: string — Additional CSS classes.
- `classNames`: { root?, option?, indicator?, badge? } — Sub-element class overrides.
- `id`: string — HTML id.
- `data-testid`: string — Test id.
#### SegmentOption Type
```ts
interface SegmentOption {
value: string; // Unique value
label?: string; // Display label (defaults to value)
icon?: ReactNode; // Icon before label
badge?: number; // Badge count (formatted via format engine)
badgeFormat?: FormatOption; // Badge format option
color?: string; // Active accent color
}
```
#### SegmentToggle Minimal Example
```tsx
```
#### SegmentToggle Examples
```tsx
// With icons and badges
},
{ value: "churned", label: "Churned", badge: 56, icon: },
]} />
```
```tsx
// Multi-select mode
```
```tsx
// Connected to FilterContext
{/* Other components read filters.dimensions.view */}
```
```tsx
// Color-coded segments
```
#### SegmentToggle Notes
- SegmentToggle is UI only — it captures the selection, not filters data.
- Without FilterProvider, use onChange for standalone mode.
- In single-select mode, a sliding indicator animates between segments.
- In multi-select mode, at least one segment must always be selected.
- Badge counts are formatted through the format engine (compact by default).
- Dense mode inherits from MetricProvider or can be set per-component.
---
### FilterTags
Category: filter
Context-driven filter chips that automatically display active filters from FilterProvider. Renders removable chips for the active period, comparison mode, and dimension filters. No manual wiring needed. Uses forwardRef.
#### FilterTags Props
- `exclude`: string[] — Fields to exclude from display. Use "_period" and "_comparison" for built-in tags.
- `include`: string[] — Whitelist — if set, only these fields show.
- `labels`: Record — Custom labels for dimension fields. Default: capitalized field name.
- `formatPeriod`: (range: DateRange, preset: PeriodPreset | null) => string — Custom period formatter.
- `formatDimension`: (field: string, values: string[]) => string — Custom dimension value formatter.
- `dismissible`: boolean — default: true — Show dismiss buttons on each chip.
- `clearAll`: boolean — default: true — Show "Clear all" button when multiple filters active.
- `clearAllLabel`: string — default: "Clear all" — Label for the clear all button.
- `onClear`: (field: string) => void — Callback when a specific filter is cleared.
- `onClearAll`: () => void — Callback when all filters are cleared.
- `maxVisible`: number — default: 0 (no limit) — Max visible chips before collapsing. Shows "+N more" button.
- `showPeriod`: boolean — default: true — Show the period filter as a tag.
- `showComparison`: boolean — default: true — Show the comparison mode as a tag.
- `dense`: boolean — default: false — Compact mode. Falls back to MetricProvider.
- `className`: string — Additional CSS classes.
- `classNames`: { root?, chip?, clearAll? } — Sub-element class overrides.
- `id`: string — HTML id.
- `data-testid`: string — Test id.
#### FilterTags Minimal Example
```tsx
```
#### FilterTags Examples
```tsx
// Full filter setup — tags auto-appear when filters are active
```
```tsx
// Custom labels and maxVisible
```
```tsx
// Exclude specific tags
```
#### FilterTags Notes
- FilterTags reads from FilterContext automatically — no manual wiring needed.
- Renders nothing when no filters are active.
- Period and comparison tags use special keys "_period" and "_comparison" for exclude/include.
- Dismiss buttons call clearDimension() / setPeriod() / setComparisonMode() on the context.
- Clear all calls filterContext.clearAll() which resets to FilterProvider defaults.
- maxVisible collapses overflow into a "+N more" button.
- Dense mode inherits from MetricProvider or can be set per-component.
---
### Nested Providers
```tsx
import { MetricProvider, KpiCard, AreaChart } from "metricui";
function App() {
return (
{/* US section */}
{/* EU section — overrides currency and locale */}
{/* Shows as "€98,7K" with German number formatting */}
{/* Dense section */}
{/* No-animation section */}
);
}
```
### Dark Mode Setup
```css
/* globals.css */
:root {
--background: #ffffff;
--foreground: #0f172a;
--card-bg: #ffffff;
--card-border: #e2e8f0;
--card-glow: #f8fafc;
--muted: #64748b;
--accent: #6366f1;
}
.dark {
--background: #0f172a;
--foreground: #f1f5f9;
--card-bg: #1e293b;
--card-border: #334155;
--card-glow: #1e293b;
--muted: #94a3b8;
--accent: #818cf8;
}
```
```tsx
/* layout.tsx */
import { ThemeProvider } from "metricui/theme";
import { MetricProvider } from "metricui";
export default function Layout({ children }) {
return (
{children}
);
}
```
---
## Linked Hover
Syncs hover state across charts. Hover a data point in one chart and crosshairs/tooltips follow in all siblings.
```tsx
import { LinkedHoverProvider } from "metricui";
```
Charts auto-participate when inside a LinkedHoverProvider — no extra prop needed. Charts match by shared `index` values.
### useLinkedHover()
Read hover state in custom components:
```tsx
import { useLinkedHover } from "metricui";
const lh = useLinkedHover();
// lh.hoveredIndex → "Mar" | null
// lh.hoveredSeries → "revenue" | null
// lh.sourceId → which component emitted
// lh.setHoveredIndex(index, sourceId?)
// lh.setHoveredSeries(seriesId, sourceId?)
```
---
## Value Flash
Opt-in hook that adds a brief highlight animation when watched data changes. You choose which components flash — it's not automatic. Useful for real-time dashboards where individual values update.
```tsx
import { useValueFlash } from "metricui";
function LiveMetric({ value }) {
const flashClass = useValueFlash(value);
return (
);
}
```
- Returns CSS class `"mu-value-flash"` when value changes, `""` otherwise
- Skips first render (no flash on mount)
- Respects `MetricProvider.animate` and `prefers-reduced-motion`
- Options: `useValueFlash(value, { duration: 800, disabled: false })`
- Works with any data type (uses deep comparison for objects/arrays)
---
## Drill-Down
Click a chart bar, donut slice, KPI card, or table row to open a detail panel. Two tiers: zero-config auto-table, or full custom content.
### Setup
Wrap your dashboard in `DrillDown.Root` and add `drillDown` to any data component:
```tsx
import { DrillDown, BarChart } from "metricui";
```
### Zero-Config (`drillDown={true}`)
Auto-generates a summary KPI row + filtered DataTable from the chart's source data. No render function needed.
```tsx
```
### Custom Content (`drillDown={(event) => ...}`)
Full control over what appears in the panel. The event contains the clicked element's data.
```tsx
(
)}
/>
```
### Presentation Mode (`drillDownMode`)
```tsx
// Slide-over (default) — slides from right, full height
// Modal — centered, compact
// Default mode on the root
...
```
### Hooks
```tsx
import { useDrillDown, useDrillDownAction } from "metricui";
// Read drill state
const { isOpen, breadcrumbs, depth, back, close } = useDrillDown();
// Imperatively open a drill panel
const openDrill = useDrillDownAction();
openDrill({ label: "Order #123", source: "custom" }, );
```
### Navigation
- Breadcrumbs shown for nested drills (up to 4 levels)
- Back arrow pops one level
- Close X closes all levels
- Escape key closes the panel
- Backdrop click closes the panel
### Supported Components
All 12 data components: BarChart, DonutChart, AreaChart, LineChart (via AreaChart), HeatMap, BarLineChart, Funnel, Waterfall, DataTable, KpiCard, StatGroup, Gauge, Callout.
### Reactive / Live Drill Content (`renderContent`)
For dashboards with streaming or frequently-updating data, drill content can stay in sync with the parent. Pass `renderContent` to `DrillDown.Root` — it runs in the parent component's render cycle with fresh data.
```tsx
function Dashboard() {
const { data } = useStreamingData();
const renderDrillContent = (trigger) => {
if (trigger.field === "edits") {
return (
);
}
return null; // falls through to stored content
};
return (
openDrill({ title: "Edits", field: "edits" }, null) }}
/>
);
}
```
For static data dashboards, don't use `renderContent` — the default stored-content approach works perfectly.
### Tooltip Action Hints
Charts with `drillDown` or `crossFilter` auto-show a subtle hint at the bottom of tooltips: "Click to drill down" or "Click to filter".
```tsx
// Auto (default) — shows hint when drillDown or crossFilter is set
// Custom text
// Disable per-chart
// Disable globally
```
### Priority
When both `drillDown` and `crossFilter` are set on the same component, `drillDown` wins
---
## FilterBar
A container component for dashboard filters with auto-generated FilterTags, badge slot, collapsible accordion, active filter count, and clear-all. Place inside a FilterProvider.
```tsx
import { FilterBar, FilterProvider, PeriodSelector, SegmentToggle, DropdownFilter } from "metricui";
{filteredCount} results}
collapsible
defaultCollapsed={false}
>
```
### FilterBar Props
- `children`: React.ReactNode (required) — Use FilterBar.Primary and FilterBar.Secondary slots.
- `tags`: boolean | FilterTagsProps — default: true — Controls FilterTags. true: auto-renders. false: hides. Object: passes through to FilterTags (e.g., `{ exclude: ['_period'], maxVisible: 3 }`).
- `badge`: React.ReactNode — Inline right-aligned badge content (result counts, status, etc.).
- `sticky`: boolean — default: false — Stick to viewport top when scrolling. Adds frosted-glass backdrop blur with 12px top offset.
- `collapsible`: boolean — default: false — Enable accordion for secondary filters.
- `defaultCollapsed`: boolean — default: false — Initial collapsed state when collapsible is true.
- `dense`: boolean — default: false — Compact mode. Falls back to MetricProvider config.
- `className`: string
- `classNames`: `{ root?, primary?, secondary?, tags?, badge? }`
- `id`: string
- `data-testid`: string
### FilterBar.Primary / FilterBar.Secondary
Named slot components that render in the primary (always visible) and secondary (collapsible) rows. No extra props — just wrap your filter controls.
```tsx
```
### Behavior
- FilterBar.Primary always stays visible. FilterBar.Secondary collapses when `collapsible` is true.
- Active filter count and clear-all button are automatically shown when filters are active.
- `tags={true}` (default) auto-renders FilterTags with sensible defaults below the filter controls.
- Must be inside a FilterProvider.
---
## Export System
Enable PNG/CSV/clipboard exports on any data component via the `exportable` prop.
### Setup
```tsx
import { MetricProvider } from "metricui";
// Enable globally for all data components
{children}
```
### ExportableConfig Type
```ts
type ExportableConfig = boolean | { data: Record[] };
```
- `true` — auto-export. Charts export source data, KPI cards export the raw numeric value (not the display-formatted string).
- `{ data: rows[] }` — override with custom data for CSV export.
- `false` — disable export on a specific component (overrides global).
### Per-Component Override
```tsx
// KpiCard: single value exported by default, override with detail data
// Charts: auto-export source data (no override needed)
// Disable export on a specific component
```
### Export Behavior
- ExportButton renders a dropdown in the card header with PNG, CSV, and clipboard options.
- Clean filenames: "Title — Filters — Date.ext" (e.g., "Revenue — Last 30 days — 2024-03-15.csv").
- Filter context metadata is included in CSV export headers.
- Charts auto-detect source data from their `data` prop — no manual `{ data }` override needed.
- KPI cards export a single value row by default. Override with `{ data: detailRows }` for a detail breakdown.
---
## CardShell (Unified Card Architecture)
CardShell is the single card wrapper used by all data components internally. You never use it directly — it powers KpiCard, all charts (via ChartContainer), DataTable, and StatusIndicator.
### Why It Matters
Any feature added to CardShell automatically works on every data component. Current CardShell features:
- Card chrome (title, subtitle, description, footnote, action slot)
- Visual variants (`variant` prop)
- Dense mode (`dense` prop)
- Data states (loading, empty, error, stale)
- Export system (`exportable` prop + ExportButton)
- Drill-down integration
- Auto empty state detection
### Auto Empty States
CardShell automatically detects when a component has no data (exportData.length === 0) and shows:
> "Nothing to show — try adjusting your filters"
Three override tiers:
1. **Automatic** — works out of the box, no props needed.
2. **Global** — `` overrides the default message for all components.
3. **Per-component** — `empty={{ message: "No revenue data", icon: , action: }}` overrides on a single component.
---
## useCrossFilteredData Hook
One-line convenience hook for applying cross-filter selections to your data.
```tsx
import { useCrossFilteredData } from "metricui";
const data = useCrossFilteredData(allData, "country");
// When "US" is selected via cross-filter: allData.filter(row => row.country === "US")
// When nothing selected: allData (unfiltered)
```
For multi-filter scenarios (e.g., dropdown + cross-filter), use `useCrossFilter()` directly.