components/we-inc/striped-comparison-table-with-sticky-header.tsx
// A striped plan comparison inside a scrolling box: the header row sticks while you scroll.
const plans = ["Commuter", "Tourer", "Cargo"];
const rows: [string, (string | boolean)[]][] = [
["Range on one charge", ["60 km", "110 km", "85 km"]],
["Motor", ["250 W hub", "250 W mid-drive", "250 W mid-drive"]],
["Weight", ["19 kg", "23 kg", "34 kg"]],
["Gears", ["Single speed", "Belt hub, 8", "Belt hub, 5"]],
["Integrated lights", [true, true, true]],
["Rear rack", [false, true, true]],
["Front cargo box", [false, false, true]],
["Suspension fork", [false, true, false]],
["Hydraulic disc brakes", [true, true, true]],
["App lock and tracking", [false, true, true]],
["Max rider and load", ["120 kg", "130 kg", "200 kg"]],
["Warranty", ["2 years", "3 years", "3 years"]],
["Price", ["£1,290", "£2,450", "£3,180"]],
];
function Mark({ on }: { on: boolean }) {
return on ? (
<span className="inline-flex items-center gap-1.5 text-teal-800"><svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.2" aria-hidden="true"><path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" /></svg><span className="sr-only">Included</span></span>
) : (
<span className="inline-flex items-center text-slate-400"><svg viewBox="0 0 16 16" className="h-4 w-4" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M4 8h8" strokeLinecap="round" /></svg><span className="sr-only">Not included</span></span>
);
}
export default function StripedComparisonTableWithStickyHeader() {
return (
<section className="bg-white px-5 py-14 sm:py-20">
<div className="mx-auto max-w-4xl">
<h2 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl">Compare Velomarsh e-bikes</h2>
<p className="mt-2 text-[15px] text-slate-600">Scroll the table; the model names stay at the top.</p>
<div className="mt-8 max-h-[420px] overflow-auto rounded-xl border border-slate-200" tabIndex={0} role="region" aria-label="E-bike comparison, scrollable">
<table className="w-full border-separate border-spacing-0 text-left text-sm">
<caption className="sr-only">Velomarsh e-bike models compared feature by feature</caption>
<thead>
<tr>
<th scope="col" className="sticky left-0 top-0 z-20 bg-slate-900 px-3 py-3 sm:px-4.5 font-medium text-slate-300">Feature</th>
{plans.map((p) => (
<th key={p} scope="col" className="sticky top-0 z-10 bg-slate-900 px-3 py-3 sm:px-4.5 text-base font-semibold text-white">{p}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(([label, vals], i) => (
<tr key={label} className={i % 2 ? "bg-white" : "bg-teal-50/60"}>
<th scope="row" className={`sticky left-0 z-[1] px-3 py-3 sm:px-4 font-medium text-slate-900 ${i % 2 ? "bg-white" : "bg-[#f3faf9]"}`}>{label}</th>
{vals.map((v, j) => (
<td key={j} className={`px-3 py-3 sm:px-4 ${label === "Price" ? "font-semibold text-slate-900" : "text-slate-700"}`}>
{typeof v === "boolean" ? <Mark on={v} /> : v}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
);
}