Line Chart
A composable line chart with tooltips, markers, and hover interactions
Preview
Installation
pnpm dlx shadcn@latest add https://ui.bklit.com/r/line-chart.jsonUsage
The chart uses a composable API where you build charts by combining components. This design allows you to mix and match features while keeping your code clean and maintainable.
Basic Example
The simplest chart with a single line:
import { LineChart, Line, ChartTooltip } from "@bklitui/ui/charts";
const data = [
{ date: new Date("2025-01-01"), users: 1200 },
{ date: new Date("2025-01-02"), users: 1350 },
{ date: new Date("2025-01-03"), users: 1100 },
// ... more data points
];
export default function SimpleChart() {
return (
<LineChart data={data}>
<Line dataKey="users" />
<ChartTooltip />
</LineChart>
);
}Multiple Lines
Display multiple metrics on the same chart:
import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts";
const data = [
{ date: new Date("2025-01-01"), users: 1200, pageviews: 4500, sessions: 800 },
{ date: new Date("2025-01-02"), users: 1350, pageviews: 4800, sessions: 920 },
// ... more data
];
export default function MultiLineChart() {
return (
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="users" stroke="var(--chart-line-primary)" />
<Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
<Line dataKey="sessions" stroke="#10b981" />
<XAxis />
<ChartTooltip />
</LineChart>
);
}Custom Data Key
By default, the chart uses date as the x-axis key. Use xDataKey for different field names:
// Data with timestamp instead of date
const apiData = [
{ timestamp: new Date("2025-01-01"), value: 100 },
{ timestamp: new Date("2025-01-02"), value: 150 },
];
<LineChart data={apiData} xDataKey="timestamp">
<Line dataKey="value" />
<ChartTooltip />
</LineChart>Chart Sizing
Control the chart's aspect ratio and add custom styling:
// Wide chart (3:1 aspect ratio)
<LineChart data={data} aspectRatio="3 / 1">
<Line dataKey="users" />
</LineChart>
// Square chart
<LineChart data={data} aspectRatio="1 / 1">
<Line dataKey="users" />
</LineChart>
// With custom margins for more breathing room
<LineChart
data={data}
margin={{ top: 60, right: 60, bottom: 60, left: 60 }}
>
<Line dataKey="users" />
</LineChart>
// Compact margins for dense layouts
<LineChart
data={data}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<Line dataKey="users" />
</LineChart>Line Customization
Fine-tune each line's appearance:
import { curveStep, curveMonotoneX, curveBasis } from "@visx/curve";
<LineChart data={data}>
{/* Thick primary line */}
<Line
dataKey="users"
stroke="var(--chart-line-primary)"
strokeWidth={3}
/>
{/* Stepped line for discrete data */}
<Line
dataKey="sessions"
stroke="#10b981"
curve={curveStep}
strokeWidth={2}
/>
{/* Smooth monotonic curve */}
<Line
dataKey="pageviews"
stroke="var(--chart-line-secondary)"
curve={curveMonotoneX}
/>
{/* Disable edge fading */}
<Line
dataKey="revenue"
stroke="#f59e0b"
fadeEdges={false}
/>
{/* No hover highlight */}
<Line
dataKey="conversions"
stroke="#ef4444"
showHighlight={false}
/>
<ChartTooltip />
</LineChart>Grid Variations
Configure grid lines for different visual styles:
// Horizontal grid only (default)
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="users" />
</LineChart>
// Vertical grid only
<LineChart data={data}>
<Grid vertical horizontal={false} />
<Line dataKey="users" />
</LineChart>
// Both horizontal and vertical
<LineChart data={data}>
<Grid horizontal vertical />
<Line dataKey="users" />
</LineChart>
// Custom grid density
<LineChart data={data}>
<Grid
horizontal
vertical
numTicksRows={10}
numTicksColumns={15}
/>
<Line dataKey="users" />
</LineChart>
// Solid grid lines instead of dashed
<LineChart data={data}>
<Grid
horizontal
strokeDasharray=""
stroke="var(--border)"
/>
<Line dataKey="users" />
</LineChart>Animation Control
Customize or disable animations:
// Faster animation (500ms)
<LineChart data={data} animationDuration={500}>
<Line dataKey="users" animate />
<ChartTooltip />
</LineChart>
// Slower, dramatic entrance (2 seconds)
<LineChart data={data} animationDuration={2000}>
<Line dataKey="users" animate />
<ChartTooltip />
</LineChart>
// Disable all line animation
<LineChart data={data}>
<Line dataKey="users" animate={false} />
<ChartTooltip />
</LineChart>Tooltip Customization
Hide Specific Tooltip Elements
// Minimal tooltip - just the content box
<LineChart data={data}>
<Line dataKey="users" />
<ChartTooltip
showCrosshair={false}
showDots={false}
showDatePill={false}
/>
</LineChart>
// Crosshair only, no date pill
<LineChart data={data}>
<Line dataKey="users" />
<ChartTooltip showDatePill={false} />
</LineChart>Custom Row Labels
<LineChart data={data}>
<Line dataKey="users" stroke="var(--chart-line-primary)" />
<Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
<ChartTooltip
rows={(point) => [
{
color: "var(--chart-line-primary)",
label: "Active Users",
value: point.users.toLocaleString(),
},
{
color: "var(--chart-line-secondary)",
label: "Page Views",
value: point.pageviews.toLocaleString(),
},
]}
/>
</LineChart>Fully Custom Content
<LineChart data={data}>
<Line dataKey="users" />
<Line dataKey="pageviews" />
<ChartTooltip
content={({ point, index }) => (
<div className="flex flex-col gap-2 p-3">
<div className="text-sm font-medium">
{point.date.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
})}
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
<span className="text-muted-foreground">Users</span>
<span className="font-mono">{point.users.toLocaleString()}</span>
<span className="text-muted-foreground">Views</span>
<span className="font-mono">{point.pageviews.toLocaleString()}</span>
<span className="text-muted-foreground">Ratio</span>
<span className="font-mono">
{(point.pageviews / point.users).toFixed(2)}x
</span>
</div>
</div>
)}
/>
</LineChart>Real-World Examples
Dashboard Metrics Card
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LineChart, Line, ChartTooltip } from "@bklitui/ui/charts";
export function MetricsCard({ title, data, dataKey, trend }) {
const latestValue = data[data.length - 1]?.[dataKey] ?? 0;
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold">
{latestValue.toLocaleString()}
</span>
<span className={trend >= 0 ? "text-green-500" : "text-red-500"}>
{trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}%
</span>
</div>
</CardHeader>
<CardContent>
<LineChart
data={data}
aspectRatio="3 / 1"
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<Line dataKey={dataKey} strokeWidth={2} />
<ChartTooltip showDatePill={false} />
</LineChart>
</CardContent>
</Card>
);
}Comparative Analytics
import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export function AnalyticsDashboard({ data }) {
return (
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="traffic">Traffic</TabsTrigger>
<TabsTrigger value="engagement">Engagement</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="users" stroke="var(--chart-line-primary)" />
<Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
<XAxis />
<ChartTooltip
rows={(point) => [
{ color: "var(--chart-line-primary)", label: "Users", value: point.users },
{ color: "var(--chart-line-secondary)", label: "Views", value: point.pageviews },
]}
/>
</LineChart>
</TabsContent>
<TabsContent value="traffic">
<LineChart data={data}>
<Grid horizontal vertical />
<Line dataKey="pageviews" stroke="var(--chart-line-primary)" strokeWidth={3} />
<XAxis />
<ChartTooltip />
</LineChart>
</TabsContent>
<TabsContent value="engagement">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="sessions" stroke="#10b981" />
<Line dataKey="bounceRate" stroke="#ef4444" />
<XAxis />
<ChartTooltip />
</LineChart>
</TabsContent>
</Tabs>
);
}With Loading State
import { LineChart, Line, Grid, ChartTooltip } from "@bklitui/ui/charts";
import { Skeleton } from "@/components/ui/skeleton";
export function ChartWithLoading({ data, isLoading }) {
if (isLoading) {
return <Skeleton className="h-[300px] w-full" />;
}
if (!data || data.length === 0) {
return (
<div className="flex h-[300px] items-center justify-center text-muted-foreground">
No data available
</div>
);
}
return (
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="value" />
<ChartTooltip />
</LineChart>
);
}Data Formatting Tips
Working with API Responses
// Transform API data with string dates
const apiResponse = [
{ timestamp: "2025-01-01T00:00:00Z", count: 1200 },
{ timestamp: "2025-01-02T00:00:00Z", count: 1350 },
];
const chartData = apiResponse.map(item => ({
date: new Date(item.timestamp),
users: item.count,
}));
<LineChart data={chartData}>
<Line dataKey="users" />
</LineChart>Aggregating Data
// Group hourly data into daily averages
function aggregateToDaily(hourlyData) {
const grouped = {};
for (const point of hourlyData) {
const dateKey = point.date.toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = { date: new Date(dateKey), values: [] };
}
grouped[dateKey].values.push(point.value);
}
return Object.values(grouped).map(group => ({
date: group.date,
value: group.values.reduce((a, b) => a + b, 0) / group.values.length,
}));
}
const dailyData = aggregateToDaily(hourlyData);
<LineChart data={dailyData}>
<Line dataKey="value" />
</LineChart>Accessibility
The chart includes several accessibility features:
- SVG elements are marked with
aria-hidden="true"as they are decorative - Keyboard users can navigate the tooltip using standard browser interactions
- Color alone is not used to convey information—values are always displayed in the tooltip
For maximum accessibility, provide a text summary or data table alongside the chart:
export function AccessibleChart({ data }) {
return (
<div>
<LineChart data={data}>
<Line dataKey="users" />
<ChartTooltip />
</LineChart>
{/* Screen reader summary */}
<p className="sr-only">
Chart showing user growth from {data[0].users.toLocaleString()} to{" "}
{data[data.length - 1].users.toLocaleString()} users over{" "}
{data.length} days.
</p>
</div>
);
}Components
LineChart
The root component that provides context to all children.
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, unknown>[] | required | Array of data points |
xDataKey | string | "date" | Key in data for x-axis values |
margin | Partial<Margin> | { top: 40, right: 40, bottom: 40, left: 40 } | Chart margins |
animationDuration | number | 1100 | Animation duration in ms |
aspectRatio | string | "2 / 1" | CSS aspect ratio |
className | string | "" | Additional CSS class |
Line
Renders a line on the chart.
| Prop | Type | Default | Description |
|---|---|---|---|
dataKey | string | required | Key in data for y values |
stroke | string | var(--chart-line-primary) | Line color |
strokeWidth | number | 2.5 | Line width |
curve | CurveFactory | curveNatural | D3 curve function |
animate | boolean | true | Enable grow animation |
fadeEdges | boolean | true | Fade line at edges |
showHighlight | boolean | true | Show highlight on hover |
Grid
Renders grid lines.
| Prop | Type | Default | Description |
|---|---|---|---|
horizontal | boolean | true | Show horizontal lines |
vertical | boolean | false | Show vertical lines |
numTicksRows | number | 5 | Number of horizontal lines |
numTicksColumns | number | 10 | Number of vertical lines |
stroke | string | var(--chart-grid) | Line color |
strokeDasharray | string | "4,4" | Dash pattern |
XAxis
Renders x-axis labels that fade when the crosshair passes.
| Prop | Type | Default | Description |
|---|---|---|---|
numTicks | number | 6 | Number of tick labels |
tickerHalfWidth | number | 50 | Fade radius for labels |
ChartTooltip
Renders the tooltip with crosshair, dots, and content box.
| Prop | Type | Default | Description |
|---|---|---|---|
showDatePill | boolean | true | Show animated date ticker |
showCrosshair | boolean | true | Show vertical crosshair |
showDots | boolean | true | Show dots on lines |
content | (props) => ReactNode | - | Custom content renderer |
rows | (point) => TooltipRow[] | - | Custom row generator |
Markers
Add markers to annotate specific dates on the chart:
import { LineChart, Line, ChartTooltip, ChartMarkers, MarkerTooltipContent, useActiveMarkers, type ChartMarker } from "@bklitui/ui/charts";
const markers: ChartMarker[] = [
{
date: new Date("2025-01-05"),
icon: "🚀",
title: "v1.2.0 Released",
description: "New chart animations",
},
{
date: new Date("2025-01-05"), // Same day - will stack!
icon: "🐛",
title: "Bug Fix",
description: "Fixed tooltip positioning",
},
];
function MyChart({ data }) {
return (
<LineChart data={data}>
<Line dataKey="users" />
<ChartMarkers items={markers} />
<ChartTooltip>
<MarkerContent markers={markers} />
</ChartTooltip>
</LineChart>
);
}
// Use the hook to get markers for the hovered date
function MarkerContent({ markers }) {
const activeMarkers = useActiveMarkers(markers);
if (activeMarkers.length === 0) return null;
return <MarkerTooltipContent markers={activeMarkers} />;
}ChartMarker Interface
interface ChartMarker {
date: Date; // Date for marker position
icon: React.ReactNode; // Icon (emoji or component)
title: string; // Tooltip title
description?: string; // Optional description
content?: React.ReactNode; // Custom tooltip content
color?: string; // Background color override
onClick?: () => void; // Click handler
href?: string; // URL to navigate to
target?: "_blank" | "_self"; // Link target
}ChartMarkers Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | ChartMarker[] | required | Array of markers |
size | number | 28 | Marker circle size |
showLines | boolean | true | Show vertical guide lines |
animate | boolean | true | Animate markers on entrance |
Custom Tooltip Content
You can fully customize the tooltip content:
<ChartTooltip
content={({ point, index }) => (
<div className="p-3">
<h3 className="font-bold">{point.date.toLocaleDateString()}</h3>
<p>Users: {point.users}</p>
<p>Views: {point.pageviews}</p>
</div>
)}
/>Or customize just the rows:
<ChartTooltip
rows={(point) => [
{ color: "var(--chart-line-primary)", label: "Active Users", value: point.users },
{ color: "var(--chart-line-secondary)", label: "Page Views", value: point.pageviews },
]}
/>Segment Selection
Add click-drag and touch segment selection to your chart with composable components. The line highlight automatically shows the selected path segment.
Basic Usage
Click and drag (or two-finger touch on mobile) to select a range. The three segment components are fully composable -- use all of them or just the ones you need:
import { LineChart, Line, Grid, XAxis, ChartTooltip, SegmentBackground, SegmentLineFrom, SegmentLineTo } from "@bklitui/ui/charts";
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="users" />
<SegmentBackground />
<SegmentLineFrom />
<SegmentLineTo />
<XAxis />
<ChartTooltip />
</LineChart>Line Variants
Each segment boundary line supports three visual styles:
{/* Dashed lines (default) */}
<SegmentLineFrom variant="dashed" />
<SegmentLineTo variant="dashed" />
{/* Solid lines */}
<SegmentLineFrom variant="solid" />
<SegmentLineTo variant="solid" />
{/* Gradient fade (transparent at top/bottom edges) */}
<SegmentLineFrom variant="gradient" />
<SegmentLineTo variant="gradient" />Background Only
Use just the background highlight without boundary lines:
<LineChart data={data}>
<Line dataKey="users" />
<SegmentBackground />
<ChartTooltip />
</LineChart>Lines Only
Use just the boundary lines without the background:
<LineChart data={data}>
<Line dataKey="users" />
<SegmentLineFrom variant="solid" />
<SegmentLineTo variant="solid" />
<ChartTooltip />
</LineChart>Reading Selection Data
Use the useChart hook inside a child component to read the active selection and update external UI (like a header card):
import { useChart } from "@bklitui/ui/charts";
function SelectionStats({ onSelectionChange }) {
const { selection, data, xAccessor } = useChart();
useEffect(() => {
if (!selection?.active) {
onSelectionChange(null);
return;
}
const startPoint = data[selection.startIndex];
const endPoint = data[selection.endIndex];
// Compute and report stats...
onSelectionChange({ startPoint, endPoint });
}, [selection, data, xAccessor, onSelectionChange]);
return null;
}SegmentBackground
| Prop | Type | Default | Description |
|---|---|---|---|
fill | string | var(--chart-segment-background) | Fill color for the selected region |
SegmentLineFrom / SegmentLineTo
| Prop | Type | Default | Description |
|---|---|---|---|
stroke | string | var(--chart-segment-line) | Line color |
strokeWidth | number | 1 | Line width |
variant | "dashed" | "solid" | "gradient" | "dashed" | Line style |
Theming
The chart uses CSS variables for theming. Define these in your CSS:
:root {
--chart-background: oklch(1 0 0);
--chart-foreground: oklch(0.145 0.004 285);
--chart-foreground-muted: oklch(0.55 0.014 260);
--chart-line-primary: oklch(0.623 0.214 255);
--chart-line-secondary: oklch(0.705 0.015 265);
--chart-crosshair: oklch(0.4 0.1828 274.34);
--chart-grid: oklch(0.9 0 0);
--chart-tooltip-foreground: oklch(0.985 0 0);
--chart-tooltip-muted: oklch(0.65 0.01 260);
--chart-marker-background: oklch(0.97 0.005 260);
--chart-marker-border: oklch(0.85 0.01 260);
--chart-marker-foreground: oklch(0.3 0.01 260);
--chart-marker-badge-background: oklch(0 0 0);
--chart-marker-badge-foreground: oklch(1 0 0);
--chart-segment-background: oklch(0.5 0 0 / 0.06);
--chart-segment-line: oklch(0.5 0 0 / 0.25);
}
.dark {
--chart-background: oklch(0.145 0 0);
--chart-foreground: oklch(0.45 0 0);
--chart-crosshair: oklch(0.45 0 0);
--chart-grid: oklch(0.25 0 0);
--chart-marker-background: oklch(0.25 0.01 260);
--chart-marker-border: oklch(0.4 0.01 260);
--chart-marker-foreground: oklch(0.9 0 0);
--chart-marker-badge-background: oklch(1 0 0);
--chart-marker-badge-foreground: oklch(0.15 0 0);
--chart-segment-background: oklch(1 0 0 / 0.06);
--chart-segment-line: oklch(1 0 0 / 0.25);
}Dependencies
This component requires the following packages:
pnpm add @visx/shape @visx/curve @visx/scale @visx/gradient @visx/responsive @visx/event @visx/grid d3-array motion react-use-measure