Line chart

A multi-series line chart with a gliding crosshair, legend toggles, and paths that morph between ranges.

datanew
Preview
Line chart preview

Signups per day, Mon, Aug 3 to Sun, Sep 20. This year, latest 1,551 signups. Last year, latest 1,130 signups.

Signups per day
DateThis yearLast year
Mon, Aug 3900 signups886 signups
Tue, Aug 41,006 signups906 signups
Wed, Aug 51,096 signups891 signups
Thu, Aug 61,155 signups846 signups
Fri, Aug 71,175 signups778 signups
Sat, Aug 81,154 signups702 signups
Sun, Aug 91,098 signups630 signups
Mon, Aug 101,017 signups576 signups
Tue, Aug 11926 signups551 signups
Wed, Aug 12844 signups560 signups
Thu, Aug 13785 signups601 signups
Fri, Aug 14761 signups670 signups
Sat, Aug 15778 signups755 signups
Sun, Aug 16834 signups843 signups
Mon, Aug 17922 signups920 signups
Tue, Aug 181,027 signups974 signups
Wed, Aug 191,134 signups996 signups
Thu, Aug 201,225 signups984 signups
Fri, Aug 211,287 signups941 signups
Sat, Aug 221,311 signups875 signups
Sun, Aug 231,293 signups798 signups
Mon, Aug 241,239 signups726 signups
Tue, Aug 251,159 signups670 signups
Wed, Aug 261,069 signups642 signups
Thu, Aug 27985 signups648 signups
Fri, Aug 28924 signups687 signups
Sat, Aug 29897 signups754 signups
Sun, Aug 30911 signups839 signups
Mon, Aug 31964 signups927 signups
Tue, Sep 11,050 signups1,005 signups
Wed, Sep 21,154 signups1,061 signups
Thu, Sep 31,261 signups1,086 signups
Fri, Sep 41,355 signups1,077 signups
Sat, Sep 51,419 signups1,035 signups
Sun, Sep 61,446 signups971 signups
Mon, Sep 71,432 signups895 signups
Tue, Sep 81,380 signups822 signups
Wed, Sep 91,302 signups764 signups
Thu, Sep 101,212 signups734 signups
Fri, Sep 111,127 signups737 signups
Sat, Sep 121,064 signups774 signups
Sun, Sep 131,034 signups839 signups
Mon, Sep 141,044 signups922 signups
Tue, Sep 151,094 signups1,011 signups
Wed, Sep 161,178 signups1,090 signups
Thu, Sep 171,281 signups1,148 signups
Fri, Sep 181,389 signups1,176 signups
Sat, Sep 191,484 signups1,169 signups
Sun, Sep 201,551 signups1,130 signups

When to use

  • Traffic, revenue, or latency over days or weeks in a dashboard card.
  • Comparing this period against a previous period or target with a dashed series.
  • Charts that switch ranges (7d, 30d, 90d) and should morph rather than redraw.

When not to use

  • Use sparkline for a tiny inline trend with no axes or tooltip.
  • Use bar-chart for discrete categories or counts per bucket.
  • Use donut-chart when the question is the share of a whole, not change over time.

shadcn CLI

Also installs (1)

  • manicat-foundation
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-chart",
  "type": "registry:ui",
  "title": "Line chart",
  "description": "A multi-series line chart with a gliding crosshair, legend toggles, and paths that morph between ranges.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json"
  ],
  "files": [
    {
      "path": "registry/components/line-chart/line-chart.tsx",
      "type": "registry:component",
      "target": "@components/manicat/line-chart/line-chart.tsx"
    },
    {
      "path": "registry/components/line-chart/line-chart.module.css",
      "type": "registry:component",
      "target": "@components/manicat/line-chart/line-chart.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/line-chart",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/line-chart",
    "markdown": "https://uiarc.dev/components/line-chart/markdown",
    "tags": [
      "data",
      "new"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { LineChart } from "@/components/manicat/line-chart/line-chart";
import type { LineChartDatum, LineChartSeries } from "@/components/manicat/line-chart/line-chart";

const DAY = 86_400_000;

function iso(date: Date): string {
  return date.toISOString().slice(0, 10);
}

function dayLabel(date: Date): string {
  return date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" });
}

function monthDay(date: Date): string {
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
}

function range(from: Date, days: number): Date[] {
  return Array.from({ length: days }, (_, index) => new Date(from.getTime() + index * DAY));
}

const lineSeries: LineChartSeries[] = [
  { key: "this-year", label: "This year" },
  { key: "last-year", label: "Last year", dashed: true },
];

const lineData: LineChartDatum[] = range(new Date(Date.UTC(2026, 7, 3, 12)), 49).map((date, index) => ({
  key: iso(date),
  label: dayLabel(date),
  axisLabel: index % 7 === 0 ? monthDay(date) : undefined,
  values: {
    "this-year": 900 + Math.round(240 * Math.sin(index / 2.4) + index * 9),
    "last-year": 700 + Math.round(200 * Math.sin(index / 2.4 + 1.2) + index * 6),
  },
}));

export function LineChartExample() {
  return <LineChart data={lineData} series={lineSeries} label="Signups per day" unit="signups" legend height={240} />;
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Year over year Two series on one axis, the comparison drawn dashed.

Year over year Two series on one axis, the comparison drawn dashed. preview

Signups per day, Mon, Aug 3 to Tue, Sep 1. This year, latest 1,050 signups. Last year, latest 1,005 signups.

Signups per day
DateThis yearLast year
Mon, Aug 3900 signups886 signups
Tue, Aug 41,006 signups906 signups
Wed, Aug 51,096 signups891 signups
Thu, Aug 61,155 signups846 signups
Fri, Aug 71,175 signups778 signups
Sat, Aug 81,154 signups702 signups
Sun, Aug 91,098 signups630 signups
Mon, Aug 101,017 signups576 signups
Tue, Aug 11926 signups551 signups
Wed, Aug 12844 signups560 signups
Thu, Aug 13785 signups601 signups
Fri, Aug 14761 signups670 signups
Sat, Aug 15778 signups755 signups
Sun, Aug 16834 signups843 signups
Mon, Aug 17922 signups920 signups
Tue, Aug 181,027 signups974 signups
Wed, Aug 191,134 signups996 signups
Thu, Aug 201,225 signups984 signups
Fri, Aug 211,287 signups941 signups
Sat, Aug 221,311 signups875 signups
Sun, Aug 231,293 signups798 signups
Mon, Aug 241,239 signups726 signups
Tue, Aug 251,159 signups670 signups
Wed, Aug 261,069 signups642 signups
Thu, Aug 27985 signups648 signups
Fri, Aug 28924 signups687 signups
Sat, Aug 29897 signups754 signups
Sun, Aug 30911 signups839 signups
Mon, Aug 31964 signups927 signups
Tue, Sep 11,050 signups1,005 signups

Keyboard interactions

KeysAction
ArrowLeft / ArrowDownMoves the crosshair to the previous point.
ArrowRight / ArrowUpMoves to the next point; from rest it starts on the latest point.
PageUp / PageDownJumps about a sixth of the range.
Home / EndJumps to the first or last point.
EscapeLets go of the crosshair.

Accessibility

  • The figure is named by label; the plot is a role="slider" whose aria-valuetext reads the date and every visible series value.
  • A visually hidden summary gives the range and latest values, and a hidden table lists every point for screen readers.
  • Legend toggles are buttons with aria-pressed; a hidden live region says Loading while loading is true.
  • The SVG and tooltip are aria-hidden, so values are never read twice.

Motion

  • Lines draw left to right the first time the chart scrolls into view.
  • New data morphs every line from the shape on screen, sampled on shared x positions, and the value scale springs to its new range while gridlines slide.
  • A hidden series flattens into the baseline as it fades; the crosshair glides between points and the tooltip follows on its own spring, flipping sides near the edge.
  • Reduced motion skips the draw, morphs, and glides, and stops the loading pulse.

Responsive behavior

  • Width follows the container through a ResizeObserver; the plot redraws in its own pixels, and axis labels thin to one per 76px.
  • Touch scrubs with pointer capture and releases on lift; touch-action: pan-y keeps vertical page scroll working over the plot.
  • A mouse scrubs on hover and releases on leave. The value gutter is a fixed 48px column.

Performance

  • Paths are written straight to the DOM from motion values, not React state, so scrubbing and morphs do not re-render the tree.
  • Smooth curves are sampled up to 16 points per segment (about 192 per line); keep data to a few hundred points per series.
  • One ResizeObserver per chart; the draw-in waits for useInView.

Notes for AI

  • Use for one to three measures over time when the trend shape matters. For category comparison use bar-chart; for parts of a whole use donut-chart.
  • Keep series and datum keys stable across range changes so lines morph instead of redrawing.
  • Set axisLabel only on the dates you want on the axis; the chart thins them further to one per 76px, always keeping the newest.
  • Drive a headline number from onActiveChange, as in metric-card or stat-card layouts.