Skip to content
LofiStack Gallery
Type: chartWeek 02

Morphing Area Chart

A dashboard-style area chart with a smooth monotone curve, gradient fill and a glowing line. The headline number and the ▲/▼ change badge sit on top. Switch ranges and both the curve and the number animate to the new data instead of jumping. Hover, or focus and use the arrow keys, to get a crosshair and tooltip. It resizes to its container and works as a full chart or a tiny sparkline.

View on GitHub
Revenue
$58.3KUp8.6%

Revenue (30D)
DateValue
Sep 10$1,921
Sep 11$1,530
Sep 12$1,213
Sep 13$1,183
Sep 14$2,230
Sep 15$2,402
Sep 16$1,886
Sep 17$2,277
Sep 18$1,930
Sep 19$1,596
Sep 20$1,429
Sep 21$2,173
Sep 22$2,009
Sep 23$2,185
Sep 24$1,949
Sep 25$1,783
Sep 26$1,642
Sep 27$1,595
Sep 28$2,568
Sep 29$2,021
Sep 30$2,545
Oct 1$2,393
Oct 2$2,386
Oct 3$1,759
Oct 4$1,575
Oct 5$1,878
Oct 6$1,798
Oct 7$2,099
Oct 8$2,231
Oct 9$2,087

Usage

usage.tsx
import { MorphingAreaChart } from "@/components/gallery/morphing-area-chart";

<MorphingAreaChart
  title="Revenue"
  valuePrefix="$"
  defaultRange="30d"
  ranges={[
    { id: "7d", label: "7D", points: last7 },   // [{ label: "Oct 9", value: 1920 }, …]
    { id: "30d", label: "30D", points: last30 },
    { id: "90d", label: "90D", points: last90 },
  ]}
/>

// Sparkline: no header, no axis
<MorphingAreaChart title="Visitors" ranges={ranges} height={90} showHeader={false} showAxis={false} />

States

Default · Morphing between ranges · Hover crosshair + tooltip · Keyboard focus (arrow keys) · Good change (green) / bad change (red), respecting higherIsBetter · Single range (no tabs) · Empty range · Sparkline (no header or axis) · Reduced motion (instant switch)

Orders
9.4KUp86.5%

Orders (7D)
DateValue
Oct 3957
Oct 41,148
Oct 51,241
Oct 61,232
Oct 71,387
Oct 81,682
Oct 91,785
Growing · emerald · 7D default
Avg. response time
140.1 msUp24.8%

Avg. response time (7D)
DateValue
Oct 3129 ms
Oct 4105 ms
Oct 5175 ms
Oct 6132 ms
Oct 7141 ms
Oct 8138 ms
Oct 9161 ms
Lower is better (higherIsBetter=false) · average
Active users
80Up105.1%

Active users (All)
DateValue
Sep 2639
Sep 2743
Sep 2858
Sep 2973
Sep 3073
Oct 158
Oct 259
Oct 347
Oct 449
Oct 567
Oct 668
Oct 756
Oct 870
Oct 980
Single range (no tabs) · last value
Refunds
$0
No data for this range yet

Refunds (7D)
DateValue
Empty range
Visitors

Visitors (30D)
DateValue
Sep 101,493
Sep 111,477
Sep 12876
Sep 131,140
Sep 141,416
Sep 151,356
Sep 161,723
Sep 171,738
Sep 181,335
Sep 19921
Sep 201,185
Sep 211,379
Sep 221,354
Sep 231,268
Sep 241,596
Sep 251,220
Sep 26944
Sep 271,283
Sep 281,917
Sep 291,575
Sep 301,769
Oct 11,424
Oct 21,561
Oct 3957
Oct 41,148
Oct 51,241
Oct 61,232
Oct 71,387
Oct 81,682
Oct 91,785
Sparkline · no header or axis

Props

  • title*

    string

    Chart title. Also used for the accessible name and data table caption.

  • ranges*

    { id; label; points: { label; value }[] }[]

    One dataset per range tab. Ranges can have different lengths; the curve still morphs.

  • defaultRange= first range

    string

    id of the range shown first.

  • summary= "sum"

    "sum" | "average" | "last"

    What the headline number shows for the active range.

  • color= "#8b5cf6"

    string

    Line, glow, gradient fill and focus ring color.

  • valuePrefix / valueSuffix= ""

    string

    Units around every value, e.g. "$" or " ms".

  • higherIsBetter= true

    boolean

    Set false for latency, cost or churn, so a rise turns the badge red.

  • height= 240

    number

    Plot height in px. Width follows the container.

  • showHeader= true

    boolean

    Title, headline, change badge and range tabs.

  • showAxis= true

    boolean

    Gridlines and axis labels. Turn off with showHeader for a sparkline.

  • morphDuration= 700

    number

    Morph animation length in ms. 0 disables it.

  • className

    string

    Extra classes for the wrapper.

Accessibility

  • Range tabs are a radiogroup with aria-checked.
  • The plot is focusable. Left and right arrows step through the points, and Home and End jump to the ends.
  • A polite live region reads the active point, e.g. “Oct 9: $1,920”.
  • A visually hidden data table gives screen readers the full series.
  • The change badge has hidden text saying “Up” or “Down”, so meaning doesn't rely on color alone.
  • prefers-reduced-motion switches ranges instantly with no morph.

Build prompt

The final prompt used to build this component
Build a reusable React 19 + TypeScript + Tailwind v4 chart component called MorphingAreaChart: a premium analytics area chart that morphs between time ranges.

Props (typed): title, ranges ({ id, label, points: { label, value }[] }[]), defaultRange, summary ("sum" | "average" | "last"), color, valuePrefix, valueSuffix, higherIsBetter, height, showHeader, showAxis, morphDuration, className.

Rendering (SVG, no chart library):
- Width follows the container via ResizeObserver; height from props. Padding leaves room for y labels (compact numbers like $1.2k) and x labels (first / middle / last, five when wide).
- Smooth curve with monotone cubic (Fritsch–Carlson) interpolation so it never overshoots. Sample the curve at a fixed 120 points so ranges with different lengths (7 / 30 / 90) can morph into each other.
- Glowing 2.5px line (drop-shadow in the line color), a vertical gradient area fill (38% → 0%), dashed gridlines at 0/25/50/75/100% of a "nice" rounded max.
- Header: title, big compact headline (sum/average/last of the range), ▲/▼ change badge (last vs first point; green when the change is good, red when bad, flipped by higherIsBetter=false; sr-only "Up"/"Down"), and segmented range tabs (radiogroup).

Morph: on tab click, animate from the currently shown shape to the new one over morphDuration ms with easeInOutCubic. Interpolate every sample, the y max and the headline number with requestAnimationFrame. Start it in the click handler (no setState in effects). Skip it under prefers-reduced-motion.

Interaction & accessibility:
- Pointer move picks the nearest data point: dashed crosshair, a dot with a soft halo, and a floating tooltip (full formatted value + label) clamped inside the chart.
- The plot is focusable (role="group" with instructions in its aria-label). ArrowLeft/Right step points, Home/End jump. Values are announced through a polite live region.
- A visually hidden <table> holds the full data. There's an empty state for ranges with no points. Dark and light mode. No dependencies.