← Back to Meridian

How Meridian was built

A light-theme precision fintech site where hand-built, self-drawing SVG charts are the design signature. Plain HTML/CSS/JS, GSAP + ScrollTrigger from a CDN, zero chart libraries, no build step.

1 · The concept

Meridian is a consumer money-management app: spending insights, auto-save, high-yield vaults. The design direction is precision SaaS — paper-white ground (#fcfcfa), ink-black type, exactly one brand color (deep emerald #0e7a4f) with fine gradient touches, and data visualization treated as the hero visual rather than decoration.

Two things carry the page: a coded app mockup (real HTML/CSS in a device frame — balance header, categorized transaction list, insight card — no fake screenshot), and a chart system where every mark is an SVG element we position ourselves.

2 · Palette — computed, not eyeballed

Chart colors were not hand-picked. The four categorical slots were run through a palette validator that checks OKLCH lightness bands, a chroma floor, color-vision-deficiency separation (Machado 2009 ΔE), and contrast against the actual chart surface:

#0e7a4f · slot 1
emerald / brand
#2a6fd6 · slot 2
blue
#eda100 · slot 3
amber
#4a3aa7 · slot 4
violet
#fcfcfa
paper surface
#0d0f0e
ink

Result: worst all-pairs CVD ΔE of 14.9 (target ≥ 12 — segments in a donut wrap, so every pair can be adjacent). One warning: amber sits below 3:1 contrast on white. The rule for that is relief — the amber donut segment carries a direct percentage label and every value also appears in the legend and the table view, so color never carries meaning alone.

Slot order is fixed. Essentials is always emerald, Lifestyle always blue, Subscriptions always amber, Savings always violet — in the donut, in the app mockup's category chips, everywhere. Color follows the entity, never its rank.

3 · Type system

RoleFaceWhy
Display / headlinesSpace Grotesk 600A geometric grotesque with just enough character; tight -.02em tracking.
Body / UIInter 400–600Neutral, reads small, disappears behind content.
Every figureIBM Plex Mono 500–600 + tabular-numsFintech numbers should feel machined. Tabular figures also stop animated counters from jittering horizontally as digits change.

4 · The SVG chart system

All charts are built by ~300 lines of vanilla JS that write inline SVG. Each chart is data-first: arrays of real, internally-consistent numbers (October's donut segments sum to $2,986, which is October's point on the area chart; the $486 weekly total is the sum of the seven bars).

4.1 · Line/area draw-on (the signature)

The stroke-dash trick: set stroke-dasharray to the path's own length, offset it fully, then animate the offset to zero. The big chart scrubs this against scroll position, so the line literally draws as you scroll into it:

const len = path.getTotalLength();
path.style.strokeDasharray  = len;
path.style.strokeDashoffset = len;

gsap.timeline({ scrollTrigger:{
    trigger:"#bigChartWrap", start:"top 78%", end:"top 30%", scrub:1 }})
  .to(path,     { strokeDashoffset:0, ease:"none" }, 0)
  .to(clipRect, { attr:{ width: plotWidth }, ease:"none" }, 0)   // area wash
  .to(dots,     { opacity:1, stagger:.055 }, .12);

The area fill can't be dash-animated (it's a closed shape), so it hides behind a <clipPath> rectangle whose width tweens in lockstep with the line — the wash appears to be poured in behind the stroke.

4.2 · Smoothing

Month points are joined with a gentle Catmull-Rom → cubic Bézier conversion (tension ÷ 6). It curves without overshooting — important, because an interpolation that swings above a real data point is a small lie.

4.3 · Donut arcs with surface gaps

Each segment is a full <circle> whose stroke-dasharray is [segmentLength, circumference − segmentLength], rotated −90° so twelve o'clock is zero. A 2.5px slice of each arc is surrendered to the background — segments are separated by the surface showing through, never by drawn borders. The reveal animates each dasharray from 0 C to its final value, staggered clockwise.

const segLen = frac * circumference - GAP;
el("circle", {
  "stroke-dasharray":  `${segLen} ${circumference - segLen}`,
  "stroke-dashoffset": -(accumulated * circumference + GAP/2),
  transform: `rotate(-90 ${cx} ${cy})`
});

4.4 · Counters with decimal easing

GSAP tweens a plain object, and an onUpdate formats it — power3.out gives the "rush then settle" feel, and the APY counter keeps two live decimals the whole way (4.60% arrives as 4.60, not 5):

gsap.to(state, { v: 4.6, duration: 1.8, ease: "power3.out",
  onUpdate: () => el.textContent = state.v.toFixed(2) });

4.5 · Bars, steps, sparklines

5 · Dataviz principles applied

6 · The coded phone mockup

The device is three nested divs (body → screen → notch) with real app UI inside: a tabular-mono balance, an insight card on the emerald-ink gradient, and a transaction list whose category chips reuse the four categorical slot colors — a small colored dot plus a text label, so the chip system matches the donut exactly. Costs a few KB, stays razor-sharp on every display, and can be art-directed in CSS forever.

7 · Generated imagery

One AI-generated asset is used — the dark topographic texture behind the security strip and download CTA (assets/texture-emerald.jpg, compressed to ~250KB), generated via Higgsfield (Nano Banana Pro) from an art-directed brief specifying the palette, line quality, composition and mood, iterated until it matched the brand's precision-engraved register. The full brief is proprietary — available to clients on request.

Everything else on the page — every chart, the phone, the icons, the logo — is code.

8 · Motion rules

9 · Deploying to Netlify

  1. The folder is already a complete static site — index.html, guide/, assets/. No build step.
  2. Drag-and-drop: go to app.netlify.com/drop and drop the meridian-finance folder. Done.
  3. Or CLI: npm i -g netlify-cli, then from the folder run netlify deploy --prod --dir .
  4. Optional custom domain + automatic HTTPS in Site settings → Domain management.
Meridian — a design showcase. All figures illustrative. Designed & built by Fable