Data Table
Sortable, selectable, paginated table with sticky header, bulk actions, empty and loading states.
Plan comparison grid with sticky first column and headers, check/dash cells and a highlighted recommended plan.
npx shadcn@latest add https://freedesignmd.com/r/pricing-comparison-table.json1/**2 * PricingComparisonTable — a plan comparison grid with a sticky first column,3 * sticky plan headers, check / dash / text cells, group headings and a4 * highlighted recommended column.5 *6 * Props: plans, groups, billing ("monthly" | "yearly"), onSelect(planId),7 * maxHeight, theme.8 * Plan: { id, name, blurb, price: { monthly, yearly } | string, cta,9 * recommended?, badge? } (a string price renders as-is, e.g. "Custom")10 * Group: { title, rows: [{ label, hint?, values: Record<planId, boolean | string> }] }11 *12 * Browser support: all evergreen browsers (position: sticky on table cells).13 * Accessibility: a real <table> — plan names are column headers, feature14 * labels are row headers (scope="row"), group titles are <th scope="rowgroup">;15 * check cells expose "Included" and dash cells "Not included" to screen16 * readers; the scroll area is focusable with a label so keyboard users can pan17 * it; CTAs name their plan ("Choose Team").18 * Reduced motion: price swaps and hover transitions are instant.19 */20import { useState, type ReactNode } from "react";21 22export interface PricingPlan {23 id: string;24 name: string;25 blurb?: string;26 price: { monthly: number; yearly: number } | string;27 cta: string;28 recommended?: boolean;29 badge?: string;30}31export interface PricingRow { label: string; hint?: string; values: Record<string, boolean | string> }32export interface PricingGroup { title: string; rows: PricingRow[] }33 34export interface PricingComparisonTableProps {35 plans: PricingPlan[];36 groups: PricingGroup[];37 billing?: "monthly" | "yearly";38 onSelect?: (planId: string) => void;39 maxHeight?: number;40 caption?: string;41 theme?: "light" | "dark";42 className?: string;43}44 45function Cell({ v }: { v: boolean | string | undefined }): ReactNode {46 if (v === true)47 return (48 <>49 <svg className="fd-pc-check" width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M4 9.4l3.2 3.2L14 5.6" /></svg>50 <span className="fd-pc-sr">Included</span>51 </>52 );53 if (!v)54 return (55 <>56 <span className="fd-pc-dash" aria-hidden="true" />57 <span className="fd-pc-sr">Not included</span>58 </>59 );60 return <span className="fd-pc-txt">{v}</span>;61}62 63export function PricingComparisonTable({ plans, groups, billing = "monthly", onSelect, maxHeight = 640, caption = "Plan comparison", theme, className }: PricingComparisonTableProps) {64 const [hs, setHs] = useState(false);65 return (66 <div className={`fd-pc${className ? " " + className : ""}`} data-fd-theme={theme}>67 <style>{css}</style>68 <div className="fd-pc-card">69 <div className="fd-pc-scroll" style={{ maxHeight }} data-hs={hs ? "" : undefined} onScroll={(e) => setHs(e.currentTarget.scrollLeft > 2)} role="region" aria-label={`${caption}, scrollable`} tabIndex={0}>70 <table className="fd-pc-table" style={{ ["--cols" as string]: plans.length }}>71 <caption className="fd-pc-sr">{caption}</caption>72 <thead>73 <tr>74 <th scope="col" className="fd-pc-corner"><span className="fd-pc-sr">Features</span><span className="fd-pc-ctitle" aria-hidden="true">Compare plans</span></th>75 {plans.map((p) => (76 <th key={p.id} scope="col" className="fd-pc-ph" data-rec={p.recommended ? "" : undefined}>77 <div className="fd-pc-pi">78 {p.recommended ? <span className="fd-pc-badge">{p.badge ?? "Most popular"}</span> : <span className="fd-pc-badge" data-ghost="" aria-hidden="true" />}79 <span className="fd-pc-name">{p.name}</span>80 <span className="fd-pc-price" key={typeof p.price === "string" ? p.price : billing}>81 {typeof p.price === "string" ? (82 <b className="fd-pc-amt fd-pc-custom">{p.price}</b>83 ) : (84 <>85 <b className="fd-pc-amt">{p.price[billing] === 0 ? "$0" : `$${p.price[billing]}`}</b>86 <small>{p.price[billing] === 0 ? "forever" : `/ user / mo${billing === "yearly" ? ", billed yearly" : ""}`}</small>87 </>88 )}89 </span>90 <button type="button" className="fd-pc-cta" data-rec={p.recommended ? "" : undefined} onClick={() => onSelect?.(p.id)} aria-label={`${p.cta}: ${p.name} plan`}>91 {p.cta}92 </button>93 </div>94 </th>95 ))}96 </tr>97 </thead>98 {groups.map((g, gi) => (99 <tbody key={g.title}>100 <tr className="fd-pc-gr">101 <th scope="rowgroup" className="fd-pc-gh">{g.title}</th>102 <td colSpan={plans.length} className="fd-pc-gfill" />103 </tr>104 {g.rows.map((r, ri) => (105 <tr key={r.label} className="fd-pc-row">106 <th scope="row" className="fd-pc-rh">107 <span className="fd-pc-label">{r.label}</span>108 {r.hint && <small>{r.hint}</small>}109 </th>110 {plans.map((p) => (111 <td key={p.id} className="fd-pc-td" data-rec={p.recommended ? "" : undefined} data-last={gi === groups.length - 1 && ri === g.rows.length - 1 ? "" : undefined}>112 <Cell v={r.values[p.id]} />113 </td>114 ))}115 </tr>116 ))}117 </tbody>118 ))}119 </table>120 </div>121 </div>122 </div>123 );124}125 126const css = `127:where(.fd-pc,.fd-pc *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none;border:0;background:none}128:where(.fd-pc,.fd-pc *)::before,:where(.fd-pc,.fd-pc *)::after{box-sizing:border-box}129:where(.fd-pc svg){display:block;flex-shrink:0}130:where(.fd-pc button){appearance:none;-webkit-appearance:none;outline:0;border-radius:0;cursor:pointer}131:where(.fd-pc table){border-collapse:separate;border-spacing:0}132.fd-pc{133 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;134 --p-bg:#fff;--p-fg:#14131A;--p-mut:#6B6978;--p-line:rgba(20,19,26,.08);--p-line2:rgba(20,19,26,.16);--p-group:#F8F7FB;--p-accent:#6D28D9;--p-accent2:#8B5CF6;--p-tint:rgba(109,40,217,.045);--p-tintline:rgba(109,40,217,.28);--p-on:#fff;--p-ring:rgba(109,40,217,.5);--p-ok:#15803D;135 width:100%;font:400 14px/20px ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-.006em;color:var(--p-fg);-webkit-font-smoothing:antialiased}136[data-fd-theme="dark"].fd-pc,[data-fd-theme="dark"] .fd-pc{--p-bg:#121118;--p-fg:#F2F1F7;--p-mut:#9C99AA;--p-line:rgba(255,255,255,.07);--p-line2:rgba(255,255,255,.15);--p-group:#17161E;--p-accent:#A78BFA;--p-accent2:#C4B5FD;--p-tint:rgba(167,139,250,.07);--p-tintline:rgba(167,139,250,.35);--p-on:#16102B;--p-ring:rgba(167,139,250,.6);--p-ok:#4ADE80}137.fd-pc-card{border-radius:20px;background:var(--p-bg);overflow:hidden;box-shadow:0 0 0 1px var(--p-line),0 1px 2px rgba(20,19,26,.04),0 20px 48px -24px rgba(20,19,26,.2)}138.fd-pc-scroll{overflow:auto;overscroll-behavior:contain}.fd-pc-scroll:focus-visible{outline:2px solid var(--p-ring);outline-offset:-2px}139.fd-pc-table{width:100%;min-width:760px;table-layout:fixed}140.fd-pc-corner{position:sticky;left:0;top:0;z-index:4;width:272px;padding:0 24px;vertical-align:bottom;text-align:left;background:var(--p-bg);box-shadow:inset 0 -1px 0 var(--p-line2)}141.fd-pc-ctitle{display:block;padding-bottom:20px;font-size:13px;line-height:16px;font-weight:600;color:var(--p-mut)}142.fd-pc-ph{position:sticky;top:0;z-index:3;padding:0 12px;vertical-align:top;text-align:left;background:var(--p-bg);box-shadow:inset 0 -1px 0 var(--p-line2)}143.fd-pc-ph[data-rec]{background:linear-gradient(var(--p-tint),var(--p-tint)),var(--p-bg);box-shadow:inset 0 -1px 0 var(--p-line2),inset 1px 0 0 var(--p-tintline),inset -1px 0 0 var(--p-tintline),inset 0 2px 0 var(--p-accent)}144.fd-pc-pi{display:flex;flex-direction:column;align-items:flex-start;gap:4px;min-height:176px;padding:22px 0 20px 8px}145.fd-pc-badge{align-self:flex-start;height:20px;padding:0 8px;border-radius:9999px;margin:-2px 0 4px;font-size:11px;line-height:20px;font-weight:600;letter-spacing:.01em;color:var(--p-on);background:linear-gradient(135deg,var(--p-accent),var(--p-accent2))}146.fd-pc-badge[data-ghost]{background:none}147.fd-pc-name{font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.015em}148.fd-pc-price{display:flex;flex-direction:column;gap:0;min-height:60px;margin-bottom:12px;animation:fd-pc-swap var(--fd-dur-3) var(--fd-ease-out)}149.fd-pc-amt{font-size:32px;line-height:40px;font-weight:600;letter-spacing:-.04em;font-variant-numeric:tabular-nums}150.fd-pc-custom{font-size:28px}151.fd-pc-price small{font-size:12px;line-height:16px;font-weight:400;color:var(--p-mut)}152.fd-pc-ph:not([data-rec]) .fd-pc-price{margin-bottom:12px}153.fd-pc-cta{margin-top:auto;display:inline-flex;align-items:center;justify-content:center;height:36px;padding:0 16px;border-radius:10px;font-size:13px;font-weight:600;line-height:1;white-space:nowrap;color:var(--p-fg);background:var(--p-bg);box-shadow:inset 0 0 0 1px var(--p-line2),0 1px 2px rgba(20,19,26,.05);transition:transform var(--fd-dur-3) var(--fd-spring),background-color var(--fd-dur-1),box-shadow var(--fd-dur-1),filter var(--fd-dur-1)}154@media (hover:hover){.fd-pc-cta:hover{background:var(--p-group);box-shadow:inset 0 0 0 1px var(--p-mut),0 1px 2px rgba(20,19,26,.06)}}155.fd-pc-cta:active{transform:scale(.96)}156.fd-pc-cta[data-rec]{color:var(--p-on);background:linear-gradient(180deg,var(--p-accent2),var(--p-accent));box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 1px 2px rgba(109,40,217,.4),0 6px 16px -6px rgba(109,40,217,.5)}157@media (hover:hover){.fd-pc-cta[data-rec]:hover{background:linear-gradient(180deg,var(--p-accent2),var(--p-accent));filter:brightness(1.08)}}158.fd-pc-cta:focus-visible{box-shadow:0 0 0 2px var(--p-bg),0 0 0 4px var(--p-ring)}159.fd-pc-gh{position:sticky;left:0;height:44px;white-space:nowrap;padding:0 24px;text-align:left;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--p-mut);background:var(--p-group);box-shadow:inset 0 1px 0 var(--p-line),inset 0 -1px 0 var(--p-line)}160.fd-pc-gh{z-index:2}161.fd-pc-gfill{height:44px;background:var(--p-group);box-shadow:inset 0 1px 0 var(--p-line),inset 0 -1px 0 var(--p-line)}162.fd-pc-rh{position:sticky;left:0;z-index:2;height:56px;padding:0 24px;text-align:left;font-weight:400;background:var(--p-bg);box-shadow:inset 0 -1px 0 var(--p-line);transition:box-shadow var(--fd-dur-2)}163.fd-pc-scroll[data-hs] .fd-pc-rh,.fd-pc-scroll[data-hs] .fd-pc-corner{box-shadow:inset 0 -1px 0 var(--p-line),8px 0 12px -8px rgba(20,19,26,.16)}164.fd-pc-scroll[data-hs] .fd-pc-corner{box-shadow:inset 0 -1px 0 var(--p-line2),8px 0 12px -8px rgba(20,19,26,.16)}165.fd-pc-label{display:block;font-weight:500}166.fd-pc-rh small{display:block;font-size:12px;line-height:16px;color:var(--p-mut)}167.fd-pc-td{height:56px;padding:0 20px;text-align:left;vertical-align:middle;box-shadow:inset 0 -1px 0 var(--p-line);transition:background-color var(--fd-dur-1)}168.fd-pc-td[data-rec]{background:var(--p-tint);box-shadow:inset 0 -1px 0 var(--p-line),inset 1px 0 0 var(--p-tintline),inset -1px 0 0 var(--p-tintline)}169.fd-pc-td[data-rec][data-last]{box-shadow:inset 1px 0 0 var(--p-tintline),inset -1px 0 0 var(--p-tintline),inset 0 -2px 0 var(--p-accent)}170.fd-pc-td[data-last]:not([data-rec]){box-shadow:none}171@media (hover:hover){.fd-pc-row:hover .fd-pc-td:not([data-rec]){background:var(--p-group)}.fd-pc-row:hover .fd-pc-rh{background:var(--p-group)}.fd-pc-row:hover .fd-pc-td[data-rec]{background:color-mix(in srgb,var(--p-accent) 8%,var(--p-bg))}}172.fd-pc-check{color:var(--p-ok)}173.fd-pc-td[data-rec] .fd-pc-check{color:var(--p-accent)}174.fd-pc-dash{display:block;width:10px;height:2px;border-radius:1px;background:var(--p-line2)}175.fd-pc-txt{font-size:14px;font-weight:500;font-variant-numeric:tabular-nums}176.fd-pc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}177@keyframes fd-pc-swap{from{opacity:0;transform:translateY(6px)}}178@media (max-width:640px){179 .fd-pc-table{min-width:680px}180 .fd-pc-corner{width:148px;padding:0 12px}.fd-pc-rh{padding:0 12px}.fd-pc-gh{padding:0 12px}181 .fd-pc-label{font-size:13px;line-height:18px}.fd-pc-rh small{display:none}182 .fd-pc-td{padding:0 12px}.fd-pc-ph{padding:0 4px}183}184@media (prefers-reduced-motion:reduce){.fd-pc *{transition-duration:1ms!important;animation:none!important}}185@media (prefers-contrast:more){.fd-pc-card{box-shadow:0 0 0 1px currentColor}}186`;187 188/* ───────────────────────────── demo ───────────────────────────── */189 190const PLANS: PricingPlan[] = [191 { id: "free", name: "Starter", price: { monthly: 0, yearly: 0 }, cta: "Start free" },192 { id: "pro", name: "Pro", price: { monthly: 24, yearly: 19 }, cta: "Choose Pro" },193 { id: "team", name: "Team", price: { monthly: 59, yearly: 47 }, cta: "Choose Team", recommended: true },194 { id: "ent", name: "Enterprise", price: "Custom", cta: "Talk to sales" },195];196 197const GROUPS: PricingGroup[] = [198 {199 title: "Usage",200 rows: [201 { label: "Tracked events", hint: "Per month, across all projects", values: { free: "10K", pro: "1M", team: "10M", ent: "Unlimited" } },202 { label: "Projects", values: { free: "1", pro: "10", team: "Unlimited", ent: "Unlimited" } },203 { label: "Data retention", values: { free: "30 days", pro: "12 months", team: "36 months", ent: "Custom" } },204 { label: "Team seats", values: { free: "1", pro: "3", team: "25", ent: "Unlimited" } },205 ],206 },207 {208 title: "Analytics",209 rows: [210 { label: "Dashboards & reports", values: { free: true, pro: true, team: true, ent: true } },211 { label: "Funnels & retention", hint: "Cohort analysis and drop-off", values: { free: false, pro: true, team: true, ent: true } },212 { label: "Session replay", values: { free: false, pro: false, team: true, ent: true } },213 { label: "SQL access", hint: "Query raw events directly", values: { free: false, pro: false, team: true, ent: true } },214 { label: "Data warehouse sync", values: { free: false, pro: false, team: false, ent: true } },215 ],216 },217 {218 title: "Collaboration",219 rows: [220 { label: "Shared dashboards", values: { free: false, pro: true, team: true, ent: true } },221 { label: "Comments & mentions", values: { free: false, pro: true, team: true, ent: true } },222 { label: "Roles & permissions", values: { free: false, pro: false, team: true, ent: true } },223 ],224 },225 {226 title: "Security & support",227 rows: [228 { label: "Single sign-on (SSO)", values: { free: false, pro: false, team: true, ent: true } },229 { label: "SAML & SCIM", values: { free: false, pro: false, team: false, ent: true } },230 { label: "Audit logs", values: { free: false, pro: false, team: false, ent: true } },231 { label: "Support", values: { free: "Community", pro: "Email, 48 h", team: "Priority, 4 h", ent: "Dedicated CSM" } },232 { label: "Uptime SLA", values: { free: false, pro: false, team: "99.9%", ent: "99.99%" } },233 ],234 },235];236 237export function Demo() {238 const [theme, setTheme] = useState<"light" | "dark">("light");239 const [billing, setBilling] = useState<"monthly" | "yearly">("yearly");240 const [chosen, setChosen] = useState("");241 return (242 <div className="fd-pcd" data-fd-theme={theme}>243 <style>{demoCss}</style>244 <header className="fd-pcd-top">245 <span className="fd-pcd-brand"><span className="fd-pcd-logo" aria-hidden="true"><i /><i /><i /></span>Plainsight</span>246 <button type="button" className="fd-pcd-theme" aria-label={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>{theme === "dark" ? "Light" : "Dark"}</button>247 </header>248 <main className="fd-pcd-main">249 <section className="fd-pcd-hero">250 <h1>Product analytics that grows with your team</h1>251 <p>Start free. Upgrade when you need funnels, replays or SSO. Cancel any time.</p>252 <div className="fd-pcd-seg" role="radiogroup" aria-label="Billing period">253 {(["monthly", "yearly"] as const).map((b) => (254 <button key={b} type="button" role="radio" aria-checked={billing === b} onClick={() => setBilling(b)} onKeyDown={(e) => { if (e.key === "ArrowRight" || e.key === "ArrowLeft") { e.preventDefault(); const n = b === "monthly" ? "yearly" : "monthly"; setBilling(n); (e.currentTarget.parentElement?.querySelector(`[data-b=${n}]`) as HTMLElement | null)?.focus(); } }} data-b={b} tabIndex={billing === b ? 0 : -1}>255 {b === "monthly" ? "Monthly" : "Yearly"}{b === "yearly" && <em>Save 20%</em>}256 </button>257 ))}258 </div>259 </section>260 <PricingComparisonTable theme={theme} plans={PLANS} groups={GROUPS} billing={billing} maxHeight={720} caption="Plainsight plan comparison" onSelect={(id) => setChosen(PLANS.find((p) => p.id === id)!.name)} />261 <p className="fd-pcd-note" role="status" aria-live="polite">{chosen ? `${chosen} selected — you’d continue to checkout from here.` : "Prices in USD. Taxes calculated at checkout."}</p>262 </main>263 </div>264 );265}266 267const demoCss = `268:where(.fd-pcd,.fd-pcd *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none;border:0;background:none}269:where(.fd-pcd,.fd-pcd *)::before,:where(.fd-pcd,.fd-pcd *)::after{box-sizing:border-box}270:where(.fd-pcd svg){display:block;flex-shrink:0}271:where(.fd-pcd button){appearance:none;-webkit-appearance:none;outline:0;border-radius:0;cursor:pointer}272.fd-pcd{--fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);273 --d-bg:#F6F5FA;--d-fg:#14131A;--d-mut:#6B6978;--d-line:rgba(20,19,26,.09);--d-accent:#6D28D9;--d-ring:rgba(109,40,217,.5);--d-card:#fff;274 min-height:100vh;background:var(--d-bg);color:var(--d-fg);font:400 14px/20px ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-.006em;-webkit-font-smoothing:antialiased}275.fd-pcd[data-fd-theme="dark"]{--d-bg:#0B0A10;--d-fg:#F2F1F7;--d-mut:#9C99AA;--d-line:rgba(255,255,255,.08);--d-accent:#A78BFA;--d-ring:rgba(167,139,250,.6);--d-card:#121118}276.fd-pcd-top{display:flex;align-items:center;justify-content:space-between;height:56px;padding:0 clamp(16px,3vw,32px)}277.fd-pcd-brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:15px;letter-spacing:-.02em}278.fd-pcd-logo{display:flex;align-items:flex-end;gap:2px;width:20px;height:20px}.fd-pcd-logo i{flex:1;border-radius:2px;background:var(--d-accent)}.fd-pcd-logo i:nth-child(1){height:45%}.fd-pcd-logo i:nth-child(2){height:75%}.fd-pcd-logo i:nth-child(3){height:100%}279.fd-pcd-theme{height:32px;padding:0 12px;border-radius:8px;font-size:13px;font-weight:500;color:var(--d-mut);box-shadow:inset 0 0 0 1px var(--d-line)}.fd-pcd-theme:hover{color:var(--d-fg)}280.fd-pcd-theme:focus-visible,.fd-pcd-seg button:focus-visible{outline:2px solid var(--d-ring);outline-offset:2px}281.fd-pcd-main{max-width:1120px;margin:0 auto;padding:32px clamp(16px,3vw,32px) 56px}282.fd-pcd-hero{display:flex;flex-direction:column;align-items:center;text-align:center;padding:24px 0 40px}283.fd-pcd-hero h1{max-width:16em;font-size:clamp(30px,5vw,48px);line-height:1.08;font-weight:600;letter-spacing:-.04em}284.fd-pcd-hero p{margin-top:14px;font-size:16px;line-height:24px;color:var(--d-mut)}285.fd-pcd-seg{position:relative;display:inline-flex;gap:2px;margin-top:28px;padding:3px;border-radius:12px;background:var(--d-line)}286.fd-pcd-seg button{display:flex;align-items:center;gap:8px;height:34px;padding:0 16px;border-radius:9px;font-size:13px;font-weight:500;line-height:1;color:var(--d-mut);transition:background-color 240ms var(--fd-ease-out),color 240ms,box-shadow 240ms}287.fd-pcd-seg button[aria-checked=true]{background:var(--d-card);color:var(--d-fg);box-shadow:0 1px 3px rgba(20,19,26,.12),0 0 0 1px var(--d-line)}288.fd-pcd-seg em{font-style:normal;font-size:11px;font-weight:600;padding:3px 6px;border-radius:9999px;color:#15803D;background:rgba(22,163,74,.12)}289.fd-pcd[data-fd-theme="dark"] .fd-pcd-seg em{color:#4ADE80}290.fd-pcd-note{margin-top:16px;text-align:center;font-size:13px;color:var(--d-mut);min-height:20px}291@media (prefers-reduced-motion:reduce){.fd-pcd *{transition-duration:1ms!important}}292`;Sortable, selectable, paginated table with sticky header, bulk actions, empty and loading states.
Team members with initial avatars, inline role menu, status pills, filter chips and a hover actions menu.
Rows that open into rich detail panels with a smooth height animation, shown with a shipment route timeline.