Skip to main content

ns-ui

Table Heat Shimmer

Dense ops table where rows above a heat threshold shimmer like air over asphalt: live DOM text refracts through an animated SVG displacement filter while cold rows sit dead-still behind hairline borders.

Usage

registry/core/table-heat-shimmer/demo.tsx
"use client";

import { useState } from "react";
import { HeatwaveLedger } from "./component";

const THRESHOLDS = [50, 70, 85] as const;

export default function HeatwaveLedgerDemo() {
  const [threshold, setThreshold] = useState<number>(70);

  return (
    <main className="flex min-h-screen items-center justify-center bg-background p-6">
      <div className="w-full max-w-3xl">
        <div className="mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
          <p className="font-mono text-xs tracking-widest text-ns-muted">
            ns-ui / table-heat-shimmer
          </p>
          <div
            role="group"
            aria-label="Heat threshold"
            className="flex items-center gap-2"
          >
            <span className="font-mono text-[11px] tracking-widest text-ns-muted">
              THRESHOLD
            </span>
            <div className="flex overflow-hidden rounded-sm border border-border">
              {THRESHOLDS.map((t) => (
                <button
                  key={t}
                  type="button"
                  aria-pressed={threshold === t}
                  onClick={() => setThreshold(t)}
                  className={`px-3 py-1.5 font-mono text-[11px] tabular-nums transition-colors duration-150 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ns-accent ${
                    threshold === t
                      ? "bg-foreground/[0.08] text-foreground"
                      : "text-ns-muted hover:bg-foreground/[0.04] hover:text-foreground"
                  }`}
                >
                  {t}
                </button>
              ))}
            </div>
          </div>
        </div>

        <HeatwaveLedger threshold={threshold} />

        <p className="mt-3 font-mono text-[11px] text-ns-muted">
          rows above the threshold shimmer like air over asphalt — hover one and
          it snaps still to read
        </p>
      </div>
    </main>
  );
}

Component

registry/core/table-heat-shimmer/component.tsx
"use client";

import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type FocusEvent,
} from "react";

// ---------------------------------------------------------------------------
// HeatwaveLedger — dense ops table where rows above a heat threshold shimmer
// like air over asphalt: real DOM text refracts through an animated SVG
// feTurbulence → feDisplacementMap filter (one filter per hot-row slot, unique
// per instance), while cold rows sit dead-still behind hairline borders. A
// pointer-events-none overlay canvas adds per-hot-row edge haze strips, fully
// cleared and redrawn each frame. The hot/cold contrast IS the piece — warmth
// is implied by motion, never by hue. One shared rAF walks only the hot list;
// hover/focus on a hot row snaps its displacement to 0 for legibility.
// ---------------------------------------------------------------------------

export type LedgerRow = {
  id: string;
  /** service name */
  service: string;
  /** deploy region */
  region: string;
  /** requests per second */
  reqs: number;
  /** p95 latency in ms */
  p95: number;
  /** heat score 0–100 — rows at/above `threshold` shimmer */
  heat: number;
};

type SortKey = "service" | "region" | "reqs" | "p95" | "heat";
type SortDir = "asc" | "desc";

type Vec3 = [number, number, number];

function parseColor(raw: string): Vec3 | null {
  const s = raw.trim();
  if (s.startsWith("#")) {
    const hex = s.slice(1);
    if (hex.length === 3) {
      const r = parseInt(hex.slice(0, 1) + hex.slice(0, 1), 16);
      const g = parseInt(hex.slice(1, 2) + hex.slice(1, 2), 16);
      const b = parseInt(hex.slice(2, 3) + hex.slice(2, 3), 16);
      return Number.isNaN(r + g + b) ? null : [r, g, b];
    }
    if (hex.length >= 6) {
      const r = parseInt(hex.slice(0, 2), 16);
      const g = parseInt(hex.slice(2, 4), 16);
      const b = parseInt(hex.slice(4, 6), 16);
      return Number.isNaN(r + g + b) ? null : [r, g, b];
    }
    return null;
  }
  const m = s.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}

function mix(a: Vec3, b: Vec3, t: number): Vec3 {
  return [
    Math.round(a[0] + (b[0] - a[0]) * t),
    Math.round(a[1] + (b[1] - a[1]) * t),
    Math.round(a[2] + (b[2] - a[2]) * t),
  ];
}

const DEFAULT_ROWS: LedgerRow[] = [
  { id: "edge-gateway", service: "edge-gateway", region: "iad-1", reqs: 12480, p95: 811, heat: 92 },
  { id: "auth-service", service: "auth-service", region: "fra-1", reqs: 8921, p95: 640, heat: 84 },
  { id: "search-index", service: "search-index", region: "sfo-2", reqs: 6115, p95: 505, heat: 76 },
  { id: "billing-api", service: "billing-api", region: "iad-1", reqs: 2210, p95: 189, heat: 48 },
  { id: "media-transcode", service: "media-transcode", region: "ams-1", reqs: 940, p95: 152, heat: 41 },
  { id: "notifications", service: "notifications", region: "sfo-2", reqs: 3308, p95: 96, heat: 34 },
  { id: "feature-flags", service: "feature-flags", region: "fra-1", reqs: 5170, p95: 22, heat: 21 },
  { id: "audit-log", service: "audit-log", region: "iad-2", reqs: 1204, p95: 64, heat: 18 },
  { id: "static-assets", service: "static-assets", region: "global", reqs: 22930, p95: 12, heat: 11 },
  { id: "cron-runner", service: "cron-runner", region: "ams-1", reqs: 88, p95: 45, heat: 7 },
];

const COLUMNS: { key: SortKey; label: string; numeric: boolean }[] = [
  { key: "service", label: "Service", numeric: false },
  { key: "region", label: "Region", numeric: false },
  { key: "reqs", label: "Req/s", numeric: true },
  { key: "p95", label: "p95 ms", numeric: true },
  { key: "heat", label: "Heat", numeric: true },
];

// shimmer numbers
const PERIOD_S = 2.2; // per-row sine period
const PHASE_S = 0.4; // phase offset per hot-row index
const SCALE_MIN = 1.5; // px displacement floor
const SCALE_MAX = 3.0; // px displacement ceiling
const RESEED_MS = 4000; // turbulence seed re-randomize interval
const ATTR_MS = 1000 / 30; // SVG attribute writes throttled to 30fps
const FREEZE_IN_MS = 120; // hover legibility snap
const FREEZE_OUT_MS = 400; // ease back on leave
const FREEZE_DEADLINE_MS = 500; // forced completion — no stranded mid-wobble
const HAZE_H = 24; // gradient strip height in px
const HAZE_ALPHA = 0.08;

type FreezeState = { v: number; from: number; target: number; start: number };

export function HeatwaveLedger({
  rows = DEFAULT_ROWS,
  threshold = 70,
  title = "Server load — last 24h",
  timestamp = "updated 09:41:07 UTC",
  className = "",
}: {
  /** table data — rows with heat >= threshold shimmer */
  rows?: LedgerRow[];
  /** heat score at/above which a row is hot */
  threshold?: number;
  /** card title */
  title?: string;
  /** mono timestamp shown beside the title */
  timestamp?: string;
  /** extra classes merged onto the rendered root element */
  className?: string;
}) {
  // unique, hydration-stable filter id prefix (useId sanitized for url(#…))
  const rawId = useId();
  const uid = useMemo(() => "heat" + rawId.replace(/[^a-zA-Z0-9_-]/g, ""), [rawId]);

  const [sortKey, setSortKey] = useState<SortKey>("heat");
  const [sortDir, setSortDir] = useState<SortDir>("desc");
  const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());
  const [reduced, setReduced] = useState(false);
  // mirrors row hover into a data-attribute alongside the CSS `hover:` classes
  // below — Chromium never advances `:hover` for the site's synthetic
  // (isTrusted: false) autoplay driver, so the cosmetic hover tint needs a JS
  // source of truth too (same pattern as button-glass).
  const [hoveredId, setHoveredId] = useState<string | null>(null);

  const wrapRef = useRef<HTMLDivElement>(null);
  const tableRef = useRef<HTMLTableElement>(null);
  const tbodyRef = useRef<HTMLTableSectionElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rowEls = useRef(new Map<string, HTMLTableRowElement>());
  const dispEls = useRef<(SVGFEDisplacementMapElement | null)[]>([]);
  const turbEls = useRef<(SVGFETurbulenceElement | null)[]>([]);
  const freezeRef = useRef(new Map<string, FreezeState>());
  const wakeRef = useRef<(() => void) | null>(null);

  const sorted = useMemo(() => {
    const arr = [...rows];
    arr.sort((a, b) => {
      const c =
        sortKey === "service" || sortKey === "region"
          ? a[sortKey].localeCompare(b[sortKey])
          : a[sortKey] - b[sortKey];
      return sortDir === "asc" ? c : -c;
    });
    return arr;
  }, [rows, sortKey, sortDir]);

  // hot rows in sorted order — index drives the shimmer phase, so sorting
  // (by heat or anything else) re-phases the field
  const hot = useMemo(
    () =>
      sorted
        .map((r, i) => ({ id: r.id, index: i }))
        .filter((_, i) => (sorted[i]?.heat ?? 0) >= threshold),
    [sorted, threshold]
  );
  const hotSlotById = useMemo(
    () => new Map(hot.map((h, k) => [h.id, k])),
    [hot]
  );
  const hotSig = useMemo(
    () => hot.map((h) => `${h.id}:${h.index}`).join("|"),
    [hot]
  );

  // reduced motion: no filter, no canvas — static accent rule instead
  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReduced(mq.matches);
    const onChange = () => setReduced(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);

  // -- hover/focus freeze: eases a hot row's displacement to 0 for legibility
  const setFreeze = (id: string, target: number) => {
    const m = freezeRef.current;
    const s = m.get(id) ?? { v: 0, from: 0, target: 0, start: 0 };
    if (s.target === target) return;
    s.from = s.v;
    s.target = target;
    s.start = performance.now();
    m.set(id, s);
    wakeRef.current?.();
  };

  // -- shared rAF: walks ONLY the hot list ---------------------------------
  useEffect(() => {
    if (reduced || hot.length === 0) return;
    const wrap = wrapRef.current;
    const table = tableRef.current;
    const tbody = tbodyRef.current;
    const canvas = canvasRef.current;
    if (!wrap || !table || !tbody || !canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const hotList = hot; // captured — effect re-runs when hotSig changes

    // haze ink: --ns-accent mixed 20% into --ns-muted, derived at mount and
    // re-derived live on documentElement class flips
    let haze: Vec3 = [114, 121, 145];
    const derive = () => {
      const cs = getComputedStyle(document.documentElement);
      const muted = parseColor(cs.getPropertyValue("--ns-muted")) ?? [143, 143, 143];
      const accent = parseColor(cs.getPropertyValue("--ns-accent")) ?? [0, 107, 255];
      haze = mix(muted, accent, 0.2);
    };
    derive();

    // overlay canvas is a replaced element — explicit style.width/height,
    // backing store scaled by dpr
    let w = 0;
    let h = 0;
    let dpr = 1;
    let sized = false;
    const resize = () => {
      const rect = wrap.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) {
        sized = false; // zero-size guard — RO wakes us when real
        return;
      }
      w = rect.width;
      h = rect.height;
      dpr = Math.min(2, window.devicePixelRatio || 1);
      canvas.width = Math.max(1, Math.round(w * dpr));
      canvas.height = Math.max(1, Math.round(h * dpr));
      canvas.style.width = `${w}px`;
      canvas.style.height = `${h}px`;
      sized = true;
    };
    resize();

    let raf = 0;
    let visible = true;
    let lastAttr = 0;
    let lastSeed = 0;

    const clearCanvas = () => {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    };

    const allFrozen = () =>
      hotList.every((hr) => {
        const s = freezeRef.current.get(hr.id);
        return s !== undefined && s.target === 1 && s.v >= 0.999;
      });

    const strip = (yFrom: number, yTo: number, a: number) => {
      const g = ctx.createLinearGradient(0, yFrom, 0, yTo);
      const [r, gg, b] = haze;
      g.addColorStop(0, `rgba(${r},${gg},${b},${a})`);
      g.addColorStop(1, `rgba(${r},${gg},${b},0)`);
      ctx.fillStyle = g;
      ctx.fillRect(0, Math.min(yFrom, yTo), w, Math.abs(yTo - yFrom));
    };

    const step = (now: number) => {
      raf = 0;
      if (!visible) return; // paused offscreen — IO wakes us

      // advance freeze transitions (ease-out cubic, hard deadline at 500ms
      // so a fast hover-scrub can never strand a row mid-wobble)
      for (const hr of hotList) {
        const s = freezeRef.current.get(hr.id);
        if (!s || s.v === s.target) continue;
        const el = now - s.start;
        const dur = s.target === 1 ? FREEZE_IN_MS : FREEZE_OUT_MS;
        if (el >= dur || el >= FREEZE_DEADLINE_MS) {
          s.v = s.target;
        } else {
          const p = el / dur;
          const e = 1 - Math.pow(1 - p, 3);
          s.v = s.from + (s.target - s.from) * e;
        }
      }

      const writeAttrs = now - lastAttr >= ATTR_MS;
      if (writeAttrs) lastAttr = now;
      // reseed rides an attribute-write frame so the throttle can't drop it
      const reseed = writeAttrs && now - lastSeed >= RESEED_MS;
      if (reseed) lastSeed = now;

      // haze canvas: fully cleared and redrawn every frame — no accumulation
      const tbodyH = tbody.offsetHeight;
      const drawable = sized && tbodyH >= 2;
      if (sized) clearCanvas();
      if (drawable) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.save();
        // clip to the tbody band — haze never bleeds into thead/footer
        const clipTop = table.offsetTop + tbody.offsetTop;
        ctx.beginPath();
        ctx.rect(0, clipTop, w, tbodyH);
        ctx.clip();
      }

      const t = now / 1000;
      for (let k = 0; k < hotList.length; k++) {
        const hr = hotList[k];
        if (!hr) continue;
        const fr = freezeRef.current.get(hr.id)?.v ?? 0;
        const osc =
          0.5 + 0.5 * Math.sin(((t - hr.index * PHASE_S) / PERIOD_S) * Math.PI * 2);
        const scale = (SCALE_MIN + (SCALE_MAX - SCALE_MIN) * osc) * (1 - fr);

        if (writeAttrs) {
          dispEls.current[k]?.setAttribute("scale", scale.toFixed(2));
          if (reseed)
            turbEls.current[k]?.setAttribute(
              "seed",
              String(Math.floor(Math.random() * 1000))
            );
        }

        if (drawable && fr < 0.999) {
          const tr = rowEls.current.get(hr.id);
          if (tr) {
            // offset coords within the wrapper (tr.offsetParent is the table,
            // table's offsetParent is the relative wrapper) — never page-absolute
            const top = table.offsetTop + tr.offsetTop;
            const bottom = top + tr.offsetHeight;
            const a = HAZE_ALPHA * (0.55 + 0.45 * osc) * (1 - fr);
            strip(top, top - HAZE_H, a); // rises off the top edge
            strip(bottom, bottom + HAZE_H, a); // sinks off the bottom edge
          }
        }
      }
      if (drawable) ctx.restore();

      // sleep: every hot row hover-frozen and settled — displacement is 0,
      // haze is 0; pin scales to exactly 0 and stop the loop entirely
      if (allFrozen()) {
        for (let k = 0; k < hotList.length; k++)
          dispEls.current[k]?.setAttribute("scale", "0");
        if (sized) clearCanvas();
        return;
      }
      raf = requestAnimationFrame(step);
    };

    const wake = () => {
      if (!raf && visible) raf = requestAnimationFrame(step);
    };
    wakeRef.current = wake;
    raf = requestAnimationFrame(step);

    const io = new IntersectionObserver((entries) => {
      visible = entries[0]?.isIntersecting ?? true;
      if (visible) wake();
    });
    io.observe(wrap);

    const ro = new ResizeObserver(() => {
      resize();
      wake();
    });
    ro.observe(wrap);

    const mo = new MutationObserver(() => {
      derive();
      wake();
    });
    mo.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });

    return () => {
      cancelAnimationFrame(raf);
      raf = 0;
      wakeRef.current = null;
      io.disconnect();
      ro.disconnect();
      mo.disconnect();
      clearCanvas();
    };
  }, [reduced, hotSig, hot]);

  const onSort = (key: SortKey) => {
    if (key === sortKey) {
      setSortDir((d) => (d === "asc" ? "desc" : "asc"));
    } else {
      setSortKey(key);
      setSortDir(key === "service" || key === "region" ? "asc" : "desc");
    }
  };

  const toggleSelect = (id: string) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const onRowBlur = (id: string) => (e: FocusEvent<HTMLTableRowElement>) => {
    // focus-within: stay frozen while focus moves inside the row
    if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;
    setFreeze(id, 0);
  };

  return (
    <div
      className={`overflow-hidden rounded-md border border-border bg-surface ${className}`}
    >
      <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-5 pb-4 pt-5">
        <h2 className="text-sm font-semibold tracking-tight text-foreground">
          {title}
        </h2>
        <span className="font-mono text-[11px] tracking-wider text-ns-muted">
          {timestamp}
        </span>
      </div>

      <div ref={wrapRef} className="relative">
        {/* one filter per hot-row slot, ids unique per instance */}
        {!reduced && hot.length > 0 && (
          <svg aria-hidden width="0" height="0" className="absolute" focusable="false">
            <defs>
              {hot.map((h, k) => (
                <filter
                  key={h.id}
                  id={`${uid}-h${k}`}
                  x="-2%"
                  y="-60%"
                  width="104%"
                  height="220%"
                  colorInterpolationFilters="sRGB"
                >
                  <feTurbulence
                    ref={(el) => {
                      turbEls.current[k] = el;
                    }}
                    type="fractalNoise"
                    baseFrequency="0.008 0.02"
                    numOctaves={2}
                    seed={7 + k * 13}
                    result="n"
                  />
                  <feDisplacementMap
                    ref={(el) => {
                      dispEls.current[k] = el;
                    }}
                    in="SourceGraphic"
                    in2="n"
                    scale="0"
                    xChannelSelector="R"
                    yChannelSelector="G"
                  />
                </filter>
              ))}
            </defs>
          </svg>
        )}

        <table ref={tableRef} className="w-full border-collapse text-sm">
          <thead>
            <tr className="border-b border-border">
              <th scope="col" className="w-10 px-5 py-2.5">
                <span className="sr-only">Select</span>
              </th>
              {COLUMNS.map((col) => {
                const active = sortKey === col.key;
                return (
                  <th
                    key={col.key}
                    scope="col"
                    aria-sort={
                      active
                        ? sortDir === "asc"
                          ? "ascending"
                          : "descending"
                        : undefined
                    }
                    className={`px-3 py-2.5 first:pl-5 last:pr-5 ${
                      col.numeric ? "text-right" : "text-left"
                    }`}
                  >
                    <button
                      type="button"
                      onClick={() => onSort(col.key)}
                      className={`inline-flex items-center gap-1 rounded-sm font-mono text-[11px] uppercase tracking-widest transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent ${
                        active ? "text-foreground" : "text-ns-muted hover:text-foreground"
                      } ${col.numeric ? "flex-row-reverse" : ""}`}
                    >
                      {col.label}
                      <svg
                        viewBox="0 0 8 8"
                        aria-hidden
                        className={`h-2 w-2 transition-opacity duration-150 ${
                          active ? "opacity-100" : "opacity-0"
                        } ${active && sortDir === "asc" ? "rotate-180" : ""}`}
                        fill="currentColor"
                      >
                        <path d="M4 6 0.8 2h6.4Z" />
                      </svg>
                    </button>
                  </th>
                );
              })}
            </tr>
          </thead>
          <tbody ref={tbodyRef}>
            {sorted.map((row) => {
              const slot = hotSlotById.get(row.id);
              const isHot = slot !== undefined;
              const isSel = selected.has(row.id);

              const style: CSSProperties = {};
              if (isHot && !reduced) style.filter = `url(#${uid}-h${slot})`;
              const shadows: string[] = [];
              if (isHot && reduced)
                shadows.push("inset 2px 0 0 0 var(--ns-accent)"); // static heat rule
              if (isSel)
                shadows.push(
                  "inset 0 0 0 1px color-mix(in srgb, var(--foreground) 35%, var(--ns-muted))"
                );
              if (shadows.length > 0) style.boxShadow = shadows.join(", ");

              // token-relative fills only: hover raises one surface step,
              // selection sits one step above that
              // (`data-[hover=true]` mirrors `:hover` for synthetic/driver input)
              const bg = isSel
                ? "bg-foreground/[0.06]"
                : isHot && reduced
                  ? "bg-foreground/[0.04] hover:bg-foreground/[0.06] data-[hover=true]:bg-foreground/[0.06]"
                  : "hover:bg-foreground/[0.04] data-[hover=true]:bg-foreground/[0.04]";

              return (
                <tr
                  key={row.id}
                  ref={(el) => {
                    if (el) rowEls.current.set(row.id, el);
                    else rowEls.current.delete(row.id);
                  }}
                  style={style}
                  aria-selected={isSel}
                  data-hover={hoveredId === row.id}
                  onPointerEnter={() => {
                    setHoveredId(row.id);
                    if (isHot && !reduced) setFreeze(row.id, 1);
                  }}
                  onPointerLeave={() => {
                    setHoveredId((h) => (h === row.id ? null : h));
                    if (isHot && !reduced) setFreeze(row.id, 0);
                  }}
                  onFocus={isHot && !reduced ? () => setFreeze(row.id, 1) : undefined}
                  onBlur={isHot && !reduced ? onRowBlur(row.id) : undefined}
                  className={`border-b border-border transition-colors duration-150 last:border-b-0 ${bg}`}
                >
                  <td className="px-5 py-3">
                    <input
                      type="checkbox"
                      checked={isSel}
                      onChange={() => toggleSelect(row.id)}
                      aria-label={`Select ${row.service}`}
                      className="block h-3.5 w-3.5 cursor-pointer"
                      style={{ accentColor: "var(--ns-accent)" }}
                    />
                  </td>
                  <td className="px-3 py-3 font-medium text-foreground">
                    {row.service}
                  </td>
                  <td className="px-3 py-3 font-mono text-xs tracking-wide text-ns-muted">
                    {row.region}
                  </td>
                  <td className="px-3 py-3 text-right font-mono text-xs tabular-nums text-ns-muted">
                    {row.reqs.toLocaleString("en-US")}
                  </td>
                  <td className="px-3 py-3 text-right font-mono text-xs tabular-nums text-ns-muted">
                    {row.p95.toLocaleString("en-US")}
                  </td>
                  <td
                    className={`px-3 py-3 pr-5 text-right font-mono text-xs tabular-nums ${
                      isHot ? "font-medium text-foreground" : "text-ns-muted"
                    }`}
                  >
                    {row.heat}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>

        {/* edge-haze overlay — explicit CSS size set in the resize handler */}
        {!reduced && hot.length > 0 && (
          <canvas
            ref={canvasRef}
            aria-hidden
            className="pointer-events-none absolute left-0 top-0"
          />
        )}
      </div>

      <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-t border-border px-5 py-3">
        <span className="font-mono text-[11px] tracking-wider text-ns-muted">
          {sorted.length} services · {hot.length} above threshold · {selected.size}{" "}
          selected
        </span>
        <div className="flex items-center gap-2">
          <button
            type="button"
            disabled
            className="rounded-sm border border-border px-2.5 py-1 font-mono text-[11px] text-ns-muted opacity-50 transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent"
          >
            Prev
          </button>
          <span className="font-mono text-[11px] tabular-nums text-ns-muted">1 / 1</span>
          <button
            type="button"
            disabled
            className="rounded-sm border border-border px-2.5 py-1 font-mono text-[11px] text-ns-muted opacity-50 transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent"
          >
            Next
          </button>
        </div>
      </div>
    </div>
  );
}

The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/table-heat-shimmer.tsx, so point the import there.

Use when a dense ops table where the rows needing attention should be findable by motion rather than by hue, hot rows refracting through a displacement filter while cold rows sit dead still; pick heatmap-calendar-tide instead when the magnitudes belong on a grid of time slots rather than in table rows, or meter-threshold-trip when one metric's trip and clear points are the question rather than which rows are hot.

Install

npx shadcn add https://design.helpmarq.com/r/table-heat-shimmer.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Build spec

Dense data table where rows above a heat threshold visibly shimmer like air over asphalt: real content refracts via an animated SVG displacement filter while cold rows sit dead-still with 1px hairline borders — the hot/cold contrast IS the piece; warmth is implied by motion, never by hue (no orange anywhere). RENDERING: semantic DOM <table> (sortable headers with real client-side comparators, selectable checkbox rows) inside a padded rounded-md surface card, plus (a) one inline SVG <filter> per hot-row slot with instance-unique ids (useId): feTurbulence type=fractalNoise baseFrequency='0.008 0.02' numOctaves=2 feeding feDisplacementMap; each hot <tr> gets style.filter=url(#heat-<id>) so its actual text/cells refract; and (b) a pointer-events:none overlay canvas over the tbody (explicit style.width/height, dpr-scaled backing store, clipped to the tbody band) drawing per-hot-row edge haze: two 24px vertical-fade gradient strips off each hot row's top and bottom edges, ink = --ns-accent mixed 20% into --ns-muted at alpha 0.08. MOTION: displacement scale oscillates 1.5→3.0px on a per-row sine (period 2.2s, phase offset 0.4s per sorted row index — resorting re-phases the field); turbulence seed re-randomized every 4s for non-looping shimmer; SVG attribute writes throttled to 30fps inside one shared rAF that walks ONLY the hot-row list and fully clears/redraws the haze canvas each frame (no accumulation). Hover or focus-within on a hot row eases its displacement to 0 over 120ms (legibility snap), back over 400ms on leave, with a forced completion deadline at 500ms so fast hover-scrubs never strand rows mid-wobble. INTERACTION: header sort (sorting by heat re-phases shimmer), checkbox selection with token-relative fill (surface step + 1px foreground/muted mix ring via color-mix), hover raises row background one surface step (bg-foreground/[0.04]). TOKENS: haze ink derived from getComputedStyle(--ns-accent, --ns-muted) at mount, re-derived live via MutationObserver on documentElement class. SLEEP/TEARDOWN: rAF stops entirely when no row exceeds threshold or all hot rows are hover-frozen (scales pinned to 0, canvas cleared); IntersectionObserver pauses offscreen; ResizeObserver with zero-size tbody guard; all listeners/observers/rAF torn down on unmount. REDUCED MOTION: no filter, no canvas — hot rows get a static 2px --ns-accent left rule and slightly elevated surface fill. DEMO: dashboard card 'Server load — last 24h' with mono timestamp, 10 believable ops rows (service, region, req/s, p95 ms, heat score), 3 hot at default threshold 70, footer row count + pagination stub, demo-level threshold segmented control.

Props

PropTypeDefaultDescription
rows?LedgerRow[]DEFAULT_ROWStable data — rows with heat >= threshold shimmer
threshold?number70heat score at/above which a row is hot
title?string"Server load — last 24h"card title
timestamp?string"updated 09:41:07 UTC"mono timestamp shown beside the title
className?string—extra classes merged onto the rendered root element