Line Chart

A composable line chart with tooltips, markers, and hover interactions

Installation

pnpm dlx shadcn@latest add @bklit/line-chart

Usage

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>
  );
}

Components

LineChart

The root component that provides context to all children.

PropTypeDefaultDescription
dataRecord<string, unknown>[]requiredArray of data points
xDataKeystring"date"Key in data for x-axis values
marginPartial<Margin>{ top: 40, right: 40, bottom: 40, left: 40 }Chart margins
animationDurationnumber1100Clip-reveal duration in ms (cubic-bezier(0.85, 0, 0.15, 1))
status"loading" | "ready""ready"Loading ↔ ready choreography on one chart instance
loadingLabelstringCentered shimmer label while status="loading" ("" hides it)
yDomainTweenbooleantrueAnimate y-domain when status or target domain changes
yDomainTweenDurationnumber500Y-domain tween duration in ms
xDomain[Date, Date]Visible x-range for brush zoom
xDomainSlotCountnumberFull dataset length for x-scale padding when xDomain is set
tweenYDomainOnXDomainChangebooleanfalseTween y-domain when the brush changes the visible x-range
aspectRatiostring"2 / 1"CSS aspect ratio
classNamestring""Additional CSS class
styleCSSPropertiesInline container styles (e.g. fixed height for a brush strip)

Line

Renders a line on the chart.

PropTypeDefaultDescription
dataKeystringrequiredKey in data for y values
yAxisIdstring | number"left"Y-scale group for biaxial charts (pair with YAxis)
strokestringvar(--chart-line-primary)Line color
strokeWidthnumber2.5Line width
curveCurveFactorycurveNaturalD3 curve function
animatebooleantrueEnable grow animation
fadeEdgesbooleantrueFade line at edges
showHighlightbooleantrueShow highlight on hover
showMarkersbooleanfalseRender scatter-style ring markers at each point
loadingStrokestringvar(--foreground)Pulse stroke color while chart is loading
loadingStrokeOpacitynumber0.5Pulse stroke opacity while chart is loading
markersSeriesPointMarkerStyleMarker styling (same options as Scatter)

Grid

Renders grid lines.

PropTypeDefaultDescription
horizontalbooleantrueShow horizontal lines
verticalbooleanfalseShow vertical lines
numTicksRowsnumber5Number of horizontal lines
numTicksColumnsnumber10Number of vertical lines
strokestringvar(--chart-grid)Line color while ready
loadingStrokestringGrid stroke while loading chrome is active
strokeDasharraystring"4,4"Dash pattern
highlightRowValuesnumber[]Draw emphasized horizontal lines at specific y values (e.g. [0] for break-even)
highlightRowStrokestringvar(--chart-foreground-muted)Stroke for highlighted rows
highlightRowStrokeOpacitynumber1Opacity for highlighted rows
highlightRowStrokeWidthnumber1Width for highlighted rows
highlightRowStrokeDasharraystring"0"Dash pattern for highlighted rows ("0" = solid)
shimmerbooleanfalseAnimate a shimmer band across horizontal grid lines
shimmerStrokestringcolor-mix(…) on --foreground at 68%Shimmer band color and opacity
shimmerLengthnumber140Shimmer band width in pixels
shimmerSpeednumber1Shimmer speed multiplier when sync is off (higher = faster)
shimmerSyncbooleanfalseMatch 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.

XAxis

Renders x-axis labels that fade when the crosshair passes.

PropTypeDefaultDescription
numTicksnumber6Number of tick labels
tickerHalfWidthnumber50Fade radius for labels
tickMode"domain" | "data""domain""domain" for evenly spaced ticks; "data" for one label per row

ChartTooltip

Renders the tooltip with crosshair, dots, and content box.

PropTypeDefaultDescription
showDatePillbooleantrueShow animated date ticker
showCrosshairbooleantrueShow vertical crosshair
showDotsbooleantrueShow dots on lines
indicatorColorstring | (point) => stringCrosshair and dot color; use a function for value-based colors
indicatorDasharraystringDash pattern for the crosshair (e.g. "4,4")
indicatorFadeEdges"both" | "top" | "bottom" | "none""both"Vertical crosshair fade
indicatorFadeLengthnumber10Fade size (% of height)
matchCrosshairbooleanfalsePanel uses crosshair spring when true
dampingnumber20Panel 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).

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.

<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 @ncdai/shimmering-text.

Loading revenue…
const [status, setStatus] = useState<"loading" | "ready">("loading");<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}  stroke="var(--chart-line-primary)"/></LineChart>

Toggle Loading / Ready in the preview to replay the transition. 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. The components tree exposes Grid, Label, and Line:

LayerControls
GridGrid and shimmer color pickers, shimmer toggle, band length, Animation (sync with line, speed when unsynced)
LabelShimmer label text
LinePulse stroke color and opacity

Data and animation panels stay collapsed in loading mode; scramble data is disabled. See the line chart gallery (Loading example).

Add @ncdai to your registry config (https://chanhdai.com/r/{name}.json) — installing @bklit/line-chart pulls in @ncdai/shimmering-text automatically.

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.

PropTypeDefaultDescription
dashFromIndexnumberInclusive data index where the dashed tail begins
dashArraystring"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

PropTypeDefaultDescription
itemsChartMarker[]requiredArray of markers
sizenumber28Marker circle size
showLinesbooleantrueShow vertical guide lines
animatebooleantrueAnimate 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

PropTypeDefaultDescription
fillstringvar(--chart-segment-background)Fill color for the selected region

SegmentLineFrom / SegmentLineTo

PropTypeDefaultDescription
strokestringvar(--chart-segment-line)Line color
strokeWidthnumber1Line 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