Line Chart
A composable line chart with tooltips, markers, and hover interactions
Installation
pnpm dlx shadcn@latest add @bklit/line-chartUsage
Build charts by composing components. See the charts gallery for interactive examples.
import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts";
const data = [
{ date: new Date("2025-01-01"), users: 1200 },
{ date: new Date("2025-01-02"), users: 1350 },
// ...
];
export default function SimpleChart() {
return (
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="users" />
<XAxis />
<ChartTooltip />
</LineChart>
);
}Updating data smoothly
When your data changes (e.g. filtering by date range, refreshing from an API), the chart automatically updates smoothly without replaying the enter animation. The y-domain tweens to the new scale via yDomainTween (enabled by default).
Do not change or pass the revealSignature prop when updating data — that prop is only for manually replaying the reveal animation (used in Studio's replay button).
const [range, setRange] = useState<"7d" | "30d" | "90d">("30d");const filteredData = useMemo(() => { const daysMap = { "7d": 7, "30d": 30, "90d": 90 }; const days = daysMap[range]; return fullData.slice(-days); }, [range]);<LineChart data={filteredData} yDomainTween> <Grid horizontal /> <Line dataKey="revenue" /> <XAxis /> <ChartTooltip /></LineChart>Click the date range buttons — the line and y-axis morph smoothly without reinitialization. Use the replay button to run the enter animation again.
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 | Clip-reveal duration in ms (cubic-bezier(0.85, 0, 0.15, 1)) |
status | "loading" | "ready" | "ready" | Loading ↔ ready choreography on one chart instance |
loadingLabel | string | — | Centered shimmer label while status="loading" ("" hides it) |
yDomainTween | boolean | true | Animate y-domain when status or target domain changes |
yDomainTweenDuration | number | 500 | Y-domain tween duration in ms |
xDomain | [Date, Date] | — | Visible x-range for brush zoom |
xDomainSlotCount | number | — | Full dataset length for x-scale padding when xDomain is set |
tweenYDomainOnXDomainChange | boolean | false | Tween y-domain when the brush changes the visible x-range |
aspectRatio | string | "2 / 1" | CSS aspect ratio |
className | string | "" | Additional CSS class |
style | CSSProperties | — | Inline container styles (e.g. fixed height for a brush strip) |
Line
Renders a line on the chart.
| Prop | Type | Default | Description |
|---|---|---|---|
dataKey | string | required | Key in data for y values |
yAxisId | string | number | "left" | Y-scale group for biaxial charts (pair with YAxis) |
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 |
showMarkers | boolean | false | Render scatter-style ring markers at each point |
loadingStroke | string | var(--foreground) | Pulse stroke color while chart is loading |
loadingStrokeOpacity | number | 0.5 | Pulse stroke opacity while chart is loading |
markers | SeriesPointMarkerStyle | — | Marker styling (same options as Scatter) |
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 while ready |
loadingStroke | string | — | Grid stroke while loading chrome is active |
strokeDasharray | string | "4,4" | Dash pattern |
highlightRowValues | number[] | — | Draw emphasized horizontal lines at specific y values (e.g. [0] for break-even) |
highlightRowStroke | string | var(--chart-foreground-muted) | Stroke for highlighted rows |
highlightRowStrokeOpacity | number | 1 | Opacity for highlighted rows |
highlightRowStrokeWidth | number | 1 | Width for highlighted rows |
highlightRowStrokeDasharray | string | "0" | Dash pattern for highlighted rows ("0" = solid) |
shimmer | boolean | false | Animate a shimmer band across horizontal grid lines |
shimmerStroke | string | color-mix(…) on --foreground at 68% | Shimmer band color and opacity |
shimmerLength | number | 140 | Shimmer band width in pixels |
shimmerSpeed | number | 1 | Shimmer speed multiplier when sync is off (higher = faster) |
shimmerSync | boolean | false | Match shimmer timing to the line pulse (2.2s cycle + 280ms pause) |
Background
Pattern fill for the plot area when you omit Grid. Fades in after the series reveal on time-series charts. See the Background utility for presets (diagonal, dots, cross, …), edge fade, and opacity — and the Pattern Background examples on the line chart gallery.
TrendLine
Least-squares regression line over one series. Place it after the Line so it draws on top. Dashed by default; also solid or gradient, with optional fadeEdges. See the Trend Line utility and the Trend Line examples on the line chart gallery.
<Line dataKey="value" />
<TrendLine dataKey="value" stroke="var(--chart-1)" />XAxis
Renders x-axis labels that fade when the crosshair passes.
| Prop | Type | Default | Description |
|---|---|---|---|
numTicks | number | ResponsiveChartLabelCount | 6 | Number of tick labels. Pass { base, sm, md, lg, xl } to vary it by chart width (640 / 768 / 1024 / 1280 px) |
tickerHalfWidth | number | 50 | Fade radius for labels |
tickMode | "data" | "domain" | "data" | "data" snaps labels to data rows (crosshair-aligned); "domain" for calendar-even spacing |
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 |
indicatorColor | string | (point) => string | — | Crosshair and dot color; use a function for value-based colors |
indicatorDasharray | string | — | Dash pattern for the crosshair (e.g. "4,4") |
indicatorFadeEdges | "both" | "top" | "bottom" | "none" | "both" | Vertical crosshair fade |
indicatorFadeLength | number | 10 | Fade size (% of height) |
matchCrosshair | boolean | false | Panel uses crosshair spring when true |
damping | number | 20 | Panel follow when matchCrosshair={false}; 0 = instant |
content | (props) => ReactNode | - | Custom content renderer |
rows | (point) => TooltipRow[] | - | Custom row generator |
Dual Y axes (biaxial)
Pair yAxisId on each Line with matching YAxis components. Increase margin.left and margin.right so labels fit. See Y Axis and the Left and right Y axes examples on the line chart gallery.
Profit/Loss line
For a single series that crosses zero, use ProfitLossLine inside LineChart. Pair it with a hidden Line (same dataKey) so the chart registers the series for the y-domain and tooltip. When any value is negative, the y-axis automatically includes the full data extent instead of anchoring at zero.
Highlight the break-even baseline with Grid highlightRowValues={[0]}:
<Grid
highlightRowValues={[0]}
highlightRowStroke="var(--foreground)"
highlightRowStrokeOpacity={0.35}
horizontal
/>See the Profit/Loss Line docs and the line chart gallery (Profit/Loss example).
Projection
Extend a series past the last data point with ProjectionLine. Build the path with buildProjectionPath (auto slope, target value, or manual points). Optionally add LineSeriesTerminalMarker at the anchor.
Projections extend the x-domain to the horizon and are not supported together with brush zoom.
Preview
Open in Studioimport {buildProjectionPath,LineSeriesTerminalMarker,ProjectionLine,} from "@bklitui/ui/charts";const projectionPath = buildProjectionPath({sourceData: chartData,seriesKey: "users",mode: "auto",autoMethod: "lastSegment",pathDensity: "endpoints",horizonPoints: 8,});<LineChart data={chartData}><Grid horizontal /><Line dataKey="users" strokeWidth={2} /><LineSeriesTerminalMarker dataKey="users" /><ProjectionLine data={projectionPath} strokeDasharray="6,4" curveKind="bezier" showEndMarker/><XAxis /><ChartTooltip /></LineChart>Open Studio with a projection via Settings → Projections or the series context menu. See the Projection Line utility for props and buildProjectionPath options.
Band
Shade a range between two per-row values — min/max, a confidence interval, or Bollinger bands — with Band. Place it before your Lines; it reveals and morphs with them and extends the y-domain to fit both edges. Use withBollingerBands to derive the edges from a series.
import { Band } from "@bklitui/ui/charts";// Each row: { date, value, valueUpper, valueLower }<LineChart data={data}> <Grid horizontal /> <Band upperKey="valueUpper" lowerKey="valueLower" strokeOpacity={0.4} /> <Line dataKey="value" /> <XAxis /> <ChartTooltip /></LineChart>See the Band utility for fill types, edge strokes, faded edges, and withBollingerBands options.
Weekends and deadlines
Shade weekends with Weekends and mark a due date with Deadline — a line at the date plus a striped region that fades out to the right. Both render behind the series and only draw what is inside the visible x-domain, so they work with brush zoom.
import { Deadline, Weekends } from "@bklitui/ui/charts";<LineChart data={data}> <Grid horizontal /> <Weekends /> <Deadline date={new Date(2026, 9, 15)} label="Due Oct 15" /> <Line dataKey="scope" stroke="var(--chart-4)" /> <Line dataKey="started" stroke="var(--chart-2)" /> <Line dataKey="completed" stroke="var(--chart-1)" /> <XAxis /> <ChartTooltip /></LineChart>Brush zoom
Use ChartBrushLayout and ChartBrush the same way as on AreaChart. See the Brush utility docs for full API reference. The brush strip typically shows simplified Line series; the main chart receives xDomain, xDomainSlotCount, and tweenYDomainOnXDomainChange for live zoom and y-domain tweening.
Preview
Open in Studio<ChartBrushLayoutdata={data}enabledheight={72}brushStrip={(layout) => ( <LineChart data={data} animationDuration={0} status="ready"> <Line dataKey="value" animate={false} /> <ChartBrush initialSelection={layout.brushSelection ?? undefined} onSelectionChange={layout.onBrushSelectionChange} /> </LineChart>)}>{(layout) => ( <LineChart data={data} xDomain={layout.xDomain} xDomainSlotCount={layout.xDomainSlotCount} tweenYDomainOnXDomainChange yDomainTween > <Grid horizontal /> <Line dataKey="value" /> <XAxis /> <ChartTooltip /> </LineChart>)}</ChartBrushLayout>Open Studio with brush enabled to tune strip height, blur, and selection pattern.
Loading state
Drive loading and ready from your data layer with a single LineChart — one Grid, one Line, no component swap. Set status="loading" while fetching; switch to "ready" when data resolves.
Loading → ready: pulse loop on skeleton data → pulse finishes its grow, then flows out right → loading label drifts down 30px, blurs, and fades → grid y-domain tween (500ms) → clip-path reveal (cubic-bezier(0.85, 0, 0.15, 1)) → interaction enabled.
Ready → loading: ready line conceals to the right → grid y-domain tween → pulse loop and shimmer resume.
Pair Grid stroke / loadingStroke with shimmer props. Pair Line loadingStroke props. Use loadingLabel on LineChart for centered shimmer text via @bklit/shimmering-text.
Preview
Open in Studioconst [status, setStatus] = useState<"loading" | "ready">("loading");const [loadingStyle, setLoadingStyle] = useState<"pulse" | "sweep">("pulse");<LineChartdata={data}status={status}loadingLabel="Loading revenue…"yDomainTween><Grid horizontal loadingStroke="color-mix(in oklch, var(--chart-grid) 50%, transparent)" shimmer shimmerSync stroke="var(--chart-grid)"/><Line dataKey="revenue" fadeEdges loadingStroke="var(--foreground)" loadingStrokeOpacity={0.5} loadingStyle={loadingStyle} stroke="var(--chart-line-primary)"/></LineChart>Toggle Loading / Ready in the preview to replay the transition, and Pulse / Sweep to switch the loading animation style. When target data spans a different y-range than the skeleton, yDomainTween morphs the scale before the line reveals.
Studio
Open Studio in loading mode and set State to Loading. In Settings, choose Loading style — Pulse (traveling segment) or Sweep (diagonal shimmer). The components tree exposes Grid, Label, and Line:
| Layer | Controls |
|---|---|
| Settings | Loading style — Pulse or Sweep (Sweep turns off grid shimmer) |
| Grid | Grid and shimmer color pickers, shimmer toggle, band length, Animation (sync with line, speed when unsynced) |
| Label | Shimmer label text |
| Line | Pulse stroke color and opacity |
Data and animation panels stay collapsed in loading mode; scramble data is disabled. See the line chart gallery (Loading example).
Installing @bklit/line-chart pulls in @bklit/shimmering-text automatically.
Loading style: pulse or sweep
The loading state has two animation styles, set with loadingStyle on the Line: the default "pulse" (a segment travels along the skeleton stroke) or "sweep" (a soft diagonal shimmer sweeps across the whole line). Set it on the Line inside a status="loading" chart, or on the LineChartLoading wrapper:
<LineChart data={data} status="loading">
<Grid horizontal shimmer />
<Line dataKey="revenue" loadingStyle="sweep" />
</LineChart>
// or, with the turnkey wrapper:
<LineChartLoading loadingStyle="sweep" />;The sweep masks over the real skeleton line, so it follows whatever curve the Line uses (curveStepAfter, curveNatural, …) and respects prefers-reduced-motion. To keep the loading→ready handoff smooth, the sweep is used only during steady loading; the pulse still drives the exit transition. See the Loading (Sweep) example on the line chart gallery.
Dashed tail
Set dashFromIndex on Line to draw a solid stroke through one data point, then a dashed segment through the end of the series. Useful when the final period is still in progress (e.g. yesterday → today).
dashFromIndex is inclusive — dashing starts at that row and continues through the last point. The dashed segment follows the same curved path as the solid stroke and respects fadeEdges.
| Prop | Type | Default | Description |
|---|---|---|---|
dashFromIndex | number | — | Inclusive data index where the dashed tail begins |
dashArray | string | "6,4" | SVG stroke-dasharray pattern for the tail segment |
<Line
dataKey="visitors"
dashFromIndex={5}
dashArray="6,4"
stroke="var(--chart-line-primary)"
/>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 |
Segment Selection
Add click-drag and touch segment selection 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:
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>Use SegmentBackground, SegmentLineFrom, and SegmentLineTo independently — you do not need all three. Boundary lines support variant="dashed" | "solid" | "gradient".
Reading Selection Data
Use the useChart hook inside a child component to read the active selection:
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-background: var(--popover);
--chart-tooltip-foreground: var(--popover-foreground);
--chart-tooltip-muted: var(--muted-foreground);
--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