← Dashboard
Zubeyr··Blockscharts

Line Chart Card

Smooth area chart comparing this period against the last, with a dotted comparison line, trend and legend.

Live demo ↗

Installation

Run the shadcn CLI in a project that already has components.json. Using the @tomei tab? Register the namespace once first.

pnpm dlx shadcn@latest add https://tomei.zubeyr.dev/ui/r/line-chart-card.json
  • →Installs recharts from npm.
  • →Pulls in card.
  • →Files land in components/dashboard.

Installing by hand? Add the dependencies, then copy the source code below.

terminal.sh
pnpm add recharts

Usage

example.tsx
import { LineChartCard } from '@/components/dashboard/line-chart-card'

const data = Array.from({ length: 30 }, (_, i) => ({
  label: `Jun ${i + 6}`,
  current: Math.round(5200 + 3600 * Math.sin(i / 2.2) + 900 * Math.cos(i / 1.3)),
  previous: Math.round(4300 + 2400 * Math.sin(i / 3.4 + 1) + 700 * Math.cos(i / 1.7)),
}))

export default function Demo() {
  return (
    <div className="grid min-h-svh place-items-center p-8">
      <LineChartCard
        title="Total sales"
        value="$250,000"
        change={42}
        data={data}
        currentLabel="Jun 6–Jul 5, 2026"
        previousLabel="May 7–Jun 5, 2026"
        formatOptions={{ style: 'currency', currency: 'USD', maximumFractionDigits: 0 }}
        className="w-full max-w-lg"
      />
    </div>
  )
}

Source code

line-chart-card.tsx
'use client'

import { useId } from 'react'
import {
  Area,
  AreaChart,
  CartesianGrid,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from 'recharts'

import { Card, LegendItem } from '@/registry/dashboard/components/card/card'

export type LinePoint = { label: string; current: number; previous?: number }

type LineChartCardProps = {
  title: React.ReactNode
  data: LinePoint[]
  value?: React.ReactNode
  change?: number
  currentLabel?: string
  previousLabel?: string
  formatOptions?: Intl.NumberFormatOptions
  action?: React.ReactNode
  height?: number
  className?: string
}

export function LineChartCard({
  title,
  data,
  value,
  change,
  currentLabel = 'This period',
  previousLabel = 'Previous period',
  formatOptions,
  action,
  height = 200,
  className,
}: LineChartCardProps) {
  const gradient = `fill-${useId().replace(/[^a-zA-Z0-9]/g, '')}`
  const full = new Intl.NumberFormat('en', formatOptions)
  const compact = new Intl.NumberFormat('en', { ...formatOptions, notation: 'compact' })
  const hasPrevious = data.some((d) => d.previous !== undefined)

  return (
    <Card
      title={title}
      value={value}
      change={change}
      action={action}
      className={className}
      legend={
        <>
          <LegendItem color="var(--chart-1)">{currentLabel}</LegendItem>
          {hasPrevious && (
            <LegendItem color="var(--chart-2)" dashed>
              {previousLabel}
            </LegendItem>
          )}
        </>
      }
    >
      <ResponsiveContainer width="100%" height={height}>
        <AreaChart data={data} margin={{ top: 8, right: 4, bottom: 0, left: 0 }}>
          <defs>
            <linearGradient id={gradient} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="var(--chart-1)" stopOpacity={0.18} />
              <stop offset="100%" stopColor="var(--chart-1)" stopOpacity={0} />
            </linearGradient>
          </defs>
          <CartesianGrid vertical={false} stroke="var(--border)" />
          <XAxis
            dataKey="label"
            axisLine={false}
            tickLine={false}
            minTickGap={32}
            tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
          />
          <YAxis
            width={44}
            axisLine={false}
            tickLine={false}
            tickCount={3}
            tickFormatter={(v: number) => compact.format(v)}
            tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
          />
          <Tooltip
            cursor={{ stroke: 'var(--border)', strokeWidth: 1 }}
            content={({ active, payload, label }) =>
              active && payload?.length ? (
                <div className="rounded-lg bg-popover px-3 py-2 text-xs text-popover-foreground shadow-(--shadow-card)">
                  <p className="mb-1 text-muted-foreground">{label}</p>
                  {payload.map((p) => (
                    <p key={String(p.dataKey)} className="flex items-center gap-2 tabular-nums">
                      <span
                        className="size-2 rounded-full"
                        style={{
                          background: p.dataKey === 'current' ? 'var(--chart-1)' : 'var(--chart-2)',
                        }}
                      />
                      {full.format(Number(p.value))}
                    </p>
                  ))}
                </div>
              ) : null
            }
          />
          {hasPrevious && (
            <Area
              type="monotone"
              dataKey="previous"
              stroke="var(--chart-2)"
              strokeWidth={2.5}
              strokeDasharray="0.5 6"
              strokeLinecap="round"
              fill="none"
              dot={false}
              activeDot={false}
            />
          )}
          <Area
            type="monotone"
            dataKey="current"
            stroke="var(--chart-1)"
            strokeWidth={2.5}
            fill={`url(#${gradient})`}
            dot={false}
            activeDot={{ r: 4, strokeWidth: 0, fill: 'var(--chart-1)' }}
          />
        </AreaChart>
      </ResponsiveContainer>
    </Card>
  )
}