Line chart
A multi-series line chart with a gliding crosshair, legend toggles, and paths that morph between ranges.
datanew
Preview
Line chart preview
Live · keyboard ready
Signups per day, Mon, Aug 3 to Sun, Sep 20. This year, latest 1,551 signups. Last year, latest 1,130 signups.
| Date | This year | Last year |
|---|---|---|
| Mon, Aug 3 | 900 signups | 886 signups |
| Tue, Aug 4 | 1,006 signups | 906 signups |
| Wed, Aug 5 | 1,096 signups | 891 signups |
| Thu, Aug 6 | 1,155 signups | 846 signups |
| Fri, Aug 7 | 1,175 signups | 778 signups |
| Sat, Aug 8 | 1,154 signups | 702 signups |
| Sun, Aug 9 | 1,098 signups | 630 signups |
| Mon, Aug 10 | 1,017 signups | 576 signups |
| Tue, Aug 11 | 926 signups | 551 signups |
| Wed, Aug 12 | 844 signups | 560 signups |
| Thu, Aug 13 | 785 signups | 601 signups |
| Fri, Aug 14 | 761 signups | 670 signups |
| Sat, Aug 15 | 778 signups | 755 signups |
| Sun, Aug 16 | 834 signups | 843 signups |
| Mon, Aug 17 | 922 signups | 920 signups |
| Tue, Aug 18 | 1,027 signups | 974 signups |
| Wed, Aug 19 | 1,134 signups | 996 signups |
| Thu, Aug 20 | 1,225 signups | 984 signups |
| Fri, Aug 21 | 1,287 signups | 941 signups |
| Sat, Aug 22 | 1,311 signups | 875 signups |
| Sun, Aug 23 | 1,293 signups | 798 signups |
| Mon, Aug 24 | 1,239 signups | 726 signups |
| Tue, Aug 25 | 1,159 signups | 670 signups |
| Wed, Aug 26 | 1,069 signups | 642 signups |
| Thu, Aug 27 | 985 signups | 648 signups |
| Fri, Aug 28 | 924 signups | 687 signups |
| Sat, Aug 29 | 897 signups | 754 signups |
| Sun, Aug 30 | 911 signups | 839 signups |
| Mon, Aug 31 | 964 signups | 927 signups |
| Tue, Sep 1 | 1,050 signups | 1,005 signups |
| Wed, Sep 2 | 1,154 signups | 1,061 signups |
| Thu, Sep 3 | 1,261 signups | 1,086 signups |
| Fri, Sep 4 | 1,355 signups | 1,077 signups |
| Sat, Sep 5 | 1,419 signups | 1,035 signups |
| Sun, Sep 6 | 1,446 signups | 971 signups |
| Mon, Sep 7 | 1,432 signups | 895 signups |
| Tue, Sep 8 | 1,380 signups | 822 signups |
| Wed, Sep 9 | 1,302 signups | 764 signups |
| Thu, Sep 10 | 1,212 signups | 734 signups |
| Fri, Sep 11 | 1,127 signups | 737 signups |
| Sat, Sep 12 | 1,064 signups | 774 signups |
| Sun, Sep 13 | 1,034 signups | 839 signups |
| Mon, Sep 14 | 1,044 signups | 922 signups |
| Tue, Sep 15 | 1,094 signups | 1,011 signups |
| Wed, Sep 16 | 1,178 signups | 1,090 signups |
| Thu, Sep 17 | 1,281 signups | 1,148 signups |
| Fri, Sep 18 | 1,389 signups | 1,176 signups |
| Sat, Sep 19 | 1,484 signups | 1,169 signups |
| Sun, Sep 20 | 1,551 signups | 1,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
Live · keyboard ready
Signups per day, Mon, Aug 3 to Tue, Sep 1. This year, latest 1,050 signups. Last year, latest 1,005 signups.
| Date | This year | Last year |
|---|---|---|
| Mon, Aug 3 | 900 signups | 886 signups |
| Tue, Aug 4 | 1,006 signups | 906 signups |
| Wed, Aug 5 | 1,096 signups | 891 signups |
| Thu, Aug 6 | 1,155 signups | 846 signups |
| Fri, Aug 7 | 1,175 signups | 778 signups |
| Sat, Aug 8 | 1,154 signups | 702 signups |
| Sun, Aug 9 | 1,098 signups | 630 signups |
| Mon, Aug 10 | 1,017 signups | 576 signups |
| Tue, Aug 11 | 926 signups | 551 signups |
| Wed, Aug 12 | 844 signups | 560 signups |
| Thu, Aug 13 | 785 signups | 601 signups |
| Fri, Aug 14 | 761 signups | 670 signups |
| Sat, Aug 15 | 778 signups | 755 signups |
| Sun, Aug 16 | 834 signups | 843 signups |
| Mon, Aug 17 | 922 signups | 920 signups |
| Tue, Aug 18 | 1,027 signups | 974 signups |
| Wed, Aug 19 | 1,134 signups | 996 signups |
| Thu, Aug 20 | 1,225 signups | 984 signups |
| Fri, Aug 21 | 1,287 signups | 941 signups |
| Sat, Aug 22 | 1,311 signups | 875 signups |
| Sun, Aug 23 | 1,293 signups | 798 signups |
| Mon, Aug 24 | 1,239 signups | 726 signups |
| Tue, Aug 25 | 1,159 signups | 670 signups |
| Wed, Aug 26 | 1,069 signups | 642 signups |
| Thu, Aug 27 | 985 signups | 648 signups |
| Fri, Aug 28 | 924 signups | 687 signups |
| Sat, Aug 29 | 897 signups | 754 signups |
| Sun, Aug 30 | 911 signups | 839 signups |
| Mon, Aug 31 | 964 signups | 927 signups |
| Tue, Sep 1 | 1,050 signups | 1,005 signups |
year-over-year-two-series-on-one-axis-the-comparison-drawn-dashed-.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;
const lineSeries: LineChartSeries[] = [
{ key: "this-year", label: "This year" },
{ key: "last-year", label: "Last year", dashed: true },
];
const lineData: LineChartDatum[] = Array.from({ length: 30 }, (_, index) => {
const date = new Date(Date.UTC(2026, 7, 3, 12) + index * DAY);
return {
key: date.toISOString().slice(0, 10),
label: date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" }),
axisLabel: index % 7 === 0 ? date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }) : 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 LineChartComparisonExample() {
return <LineChart data={lineData} series={lineSeries} label="Signups per day" unit="signups" legend height={240} />;
}
Keyboard interactions
| Keys | Action |
|---|---|
| ArrowLeft / ArrowDown | Moves the crosshair to the previous point. |
| ArrowRight / ArrowUp | Moves to the next point; from rest it starts on the latest point. |
| PageUp / PageDown | Jumps about a sixth of the range. |
| Home / End | Jumps to the first or last point. |
| Escape | Lets 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.