Infinite Data Grid

Loads more rows as the user scrolls.

Activity feed
OrderCustomerStatusTotal
#1042A. WhitfieldShipped£64.00
#1041R. OkaforProcessing£128.50
#1040M. IqbalShipped£22.00
#1039S. NovakRefunded£40.00

html

<div class="idg" style="--accent:#241430;">
  <div class="idg__scroll">
    <table class="idg__table">
      <caption>Activity feed</caption>
      <thead><tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">Status</th><th scope="col">Total</th></tr></thead>
      <tbody>
        <tr style="height:40px;"><td>#1042</td><td>A. Whitfield</td><td>Shipped</td><td>£64.00</td></tr>
        <tr style="height:40px;"><td>#1041</td><td>R. Okafor</td><td>Processing</td><td>£128.50</td></tr>
        <tr style="height:40px;"><td>#1040</td><td>M. Iqbal</td><td>Shipped</td><td>£22.00</td></tr>
        <tr style="height:40px;"><td>#1039</td><td>S. Novak</td><td>Refunded</td><td>£40.00</td></tr>
        
        <tr class="idg__skeleton" style="height:40px;" aria-hidden="true"><td colspan="4"><span class="idg__shimmer"></span></td></tr>
        
      </tbody>
    </table>
  </div>
  <div class="idg__footer">
    <div class="idg__track"><div class="idg__fill" style="width:60%;"></div></div>
    <span class="idg__status" role="status">Loading more&#8230; 60% rendered</span>
  </div>
</div>
<style>
.idg { font: 14px system-ui, sans-serif; color: #241430; background: #FAF7F2; max-width: 480px; border: 1px solid #24143020; border-radius: 10px; overflow: hidden; }
.idg__scroll { max-height: 220px; overflow-y: auto; }
.idg__table { border-collapse: collapse; width: 100%; }
.idg__table caption { text-align: left; font-weight: 600; padding: 10px 14px; }
.idg__table th, .idg__table td { padding: 0 14px; text-align: left; border-bottom: 1px solid #24143020; white-space: nowrap; }
.idg__table th { background: var(--accent); color: #FAF7F2; padding: 10px 14px; }
.idg__shimmer { display: block; height: 12px; margin: auto 0; border-radius: 4px; background: linear-gradient(90deg, #24143014 25%, #24143028 37%, #24143014 63%); background-size: 400% 100%; }
@media (prefers-reduced-motion: no-preference) {
.idg__shimmer { animation: idg-shimmer .22s ease infinite; }
}
@keyframes idg-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
.idg__footer { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-top: 1px solid #24143020; }
.idg__track { flex: 1; height: 6px; border-radius: 999px; background: #24143014; overflow: hidden; }
.idg__fill { height: 100%; background: var(--accent); border-radius: inherit; }
@media (prefers-reduced-motion: no-preference) {
.idg__fill { transition: width .2s; }
}
.idg__status { font-size: 12px; white-space: nowrap; }
</style>

tailwind

<div class="max-w-[480px] rounded-[10px] border border-[#24143020] font-sans text-sm text-[#241430] bg-[#FAF7F2] overflow-hidden" style="--accent:#241430;">
  <div class="max-h-[220px] overflow-y-auto">
    <table class="border-collapse w-full">
      <caption class="text-left font-semibold px-3.5 py-2.5">Activity feed</caption>
      <thead><tr class="text-[#FAF7F2]" style="background:#241430">
        <th scope="col" class="p-3.5 text-left whitespace-nowrap">Order</th>
        <th scope="col" class="p-3.5 text-left whitespace-nowrap">Customer</th>
        <th scope="col" class="p-3.5 text-left whitespace-nowrap">Status</th>
        <th scope="col" class="p-3.5 text-left whitespace-nowrap">Total</th>
      </tr></thead>
      <tbody>
        <tr style="height:40px"><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">#1042</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">A. Whitfield</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">Shipped</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">£64.00</td></tr>
        <tr style="height:40px"><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">#1041</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">R. Okafor</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">Processing</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">£128.50</td></tr>
        <tr style="height:40px"><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">#1040</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">M. Iqbal</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">Shipped</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">£22.00</td></tr>
        <tr style="height:40px"><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">#1039</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">S. Novak</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">Refunded</td><td class="px-3.5 border-b border-[#24143020] whitespace-nowrap">£40.00</td></tr>
        
        <tr aria-hidden="true" style="height:40px"><td colspan="4" class="px-3.5"><span class="block h-3 my-auto rounded motion-safe:animate-pulse bg-[#24143020]"></span></td></tr>
        
      </tbody>
    </table>
  </div>
  <div class="flex items-center gap-2.5 px-3.5 py-2.5 border-t border-[#24143020]">
    <div class="flex-1 h-1.5 rounded-full bg-[#24143014] overflow-hidden"><div class="h-full rounded-full" style="width:60%;background:#241430"></div></div>
    <span role="status" class="text-xs whitespace-nowrap">Loading more&#8230; 60% rendered</span>
  </div>
</div>

react

function InfiniteDataGrid({
  caption = "Activity feed",
  accent = "#241430",
  loading = true,
  loadedPercent = 60,
  rowHeight = 40,
}) {
  const rows = [
    ["#1042", "A. Whitfield", "Shipped", "£64.00"],
    ["#1041", "R. Okafor", "Processing", "£128.50"],
    ["#1040", "M. Iqbal", "Shipped", "£22.00"],
    ["#1039", "S. Novak", "Refunded", "£40.00"],
  ];
  const cell = { padding: "0 14px", borderBottom: "1px solid #24143020", whiteSpace: "nowrap" };
  return (
    <div style={{ maxWidth: 480, borderRadius: 10, border: "1px solid #24143020", overflow: "hidden", font: "14px system-ui, sans-serif", color: "#241430", background: "#FAF7F2" }}>
      <div style={{ maxHeight: 220, overflowY: "auto" }}>
        <table style={{ borderCollapse: "collapse", width: "100%" }}>
          <caption style={{ textAlign: "left", fontWeight: 600, padding: "10px 14px" }}>{caption}</caption>
          <thead>
            <tr style={{ background: accent, color: "#FAF7F2" }}>
              {["Order", "Customer", "Status", "Total"].map((h) => <th key={h} scope="col" style={{ padding: "10px 14px", textAlign: "left" }}>{h}</th>)}
            </tr>
          </thead>
          <tbody>
            {rows.map((row) => (
              <tr key={row[0]} style={{ height: rowHeight }}>{row.map((c, j) => <td key={j} style={cell}>{c}</td>)}</tr>
            ))}
            {loading && (
              <tr aria-hidden="true" style={{ height: rowHeight }}>
                <td colSpan={4} style={{ padding: "0 14px" }}>
                  <span style={{ display: "block", height: 12, borderRadius: 4, background: "#24143020" }} />
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 14px", borderTop: "1px solid #24143020" }}>
        <div style={{ flex: 1, height: 6, borderRadius: 999, background: "#24143014", overflow: "hidden" }}>
          <div style={{ height: "100%", width: `${loadedPercent}%`, background: accent, borderRadius: "inherit" }} />
        </div>
        <span role="status" style={{ fontSize: 12, whiteSpace: "nowrap" }}>{loading ? "Loading more… " : ""}{loadedPercent}% rendered</span>
      </div>
    </div>
  );
}