# Isometric Cubes (`isometric-cubes`)

> An isometric cube field — a wave rolls diagonally through the grid while cubes near the cursor rise to meet it. A compute-grid illustration for bento cards.

- **Docs:** https://www.mellowui.com/components/isometric-cubes
- **Markdown:** https://www.mellowui.com/components/isometric-cubes.md
- **Registry:** https://www.mellowui.com/r/isometric-cubes.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/isometric-cubes
- **Categories:** display, 3d, interactive, animation
- **Dependencies:** none

## AI prompt

Add an IsometricCubes component from the mellow library — an isometric cube field. A diagonal wave rolls through the grid on a sine driver while cubes near the cursor rise to meet it. Tune `rows`, `cols`, `speed` and `amplitude`. Good as a compute / infrastructure illustration for bento cards and hero sections.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/isometric-cubes.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `rows` | `number` | `5` | Grid rows. |
| `cols` | `number` | `5` | Grid columns. |
| `speed` | `number` | `1` | Wave speed multiplier. |
| `amplitude` | `number` | `14` | Maximum wave lift in world units. |
| `size` | `number` | `380` | Rendered width in px. |
| `className` | `string` | — | Additional CSS classes. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useEffect, useRef } from "react";

export interface IsometricCubesProps {
  rows?: number;
  cols?: number;
  /** Wave speed multiplier. */
  speed?: number;
  /** Maximum wave lift in world units. */
  amplitude?: number;
  /** Rendered width in px. */
  size?: number;
  className?: string;
  style?: React.CSSProperties;
}

// P(x, y, z) -> screen
const iso = (x: number, y: number, z: number): [number, number] => [
  0.866 * (x - y),
  0.5 * (x + y) - z,
];
const poly = (pts: [number, number][]) =>
  pts.map((p) => `${p[0].toFixed(2)},${p[1].toFixed(2)}`).join(" ");

const CELL = 38;
const S = 14; // cube half-size
const BASE = 10;

const TOP = "color-mix(in oklab, var(--ink) 6%, var(--background))";
const RIGHT = "color-mix(in oklab, var(--ink) 16%, var(--background))";
const FRONT = "color-mix(in oklab, var(--ink) 26%, var(--background))";

function cubeFaces(cx: number, cy: number, h: number) {
  return {
    top: poly([
      iso(cx - S, cy - S, h),
      iso(cx + S, cy - S, h),
      iso(cx + S, cy + S, h),
      iso(cx - S, cy + S, h),
    ]),
    right: poly([
      iso(cx + S, cy - S, h),
      iso(cx + S, cy + S, h),
      iso(cx + S, cy + S, 0),
      iso(cx + S, cy - S, 0),
    ]),
    front: poly([
      iso(cx + S, cy + S, h),
      iso(cx - S, cy + S, h),
      iso(cx - S, cy + S, 0),
      iso(cx + S, cy + S, 0),
    ]),
  };
}

/**
 * An isometric cube field — a wave rolls diagonally through the grid while
 * cubes near the cursor rise to meet it. A compute-grid illustration for
 * bento cards and hero sections.
 */
export function IsometricCubes({
  rows = 5,
  cols = 5,
  speed = 1,
  amplitude = 14,
  size = 380,
  className,
  style,
}: IsometricCubesProps) {
  const svgRef = useRef<SVGSVGElement>(null);
  const cellRefs = useRef<
    { top: SVGPolygonElement | null; right: SVGPolygonElement | null; front: SVGPolygonElement | null }[]
  >([]);

  // back-to-front paint order
  const cells: { cx: number; cy: number; idx: number }[] = [];
  for (let j = 0; j < rows; j++) {
    for (let i = 0; i < cols; i++) {
      cells.push({
        cx: (i - (cols - 1) / 2) * CELL,
        cy: (j - (rows - 1) / 2) * CELL,
        idx: j * cols + i,
      });
    }
  }
  cells.sort((a, b) => a.cx + a.cy - (b.cx + b.cy));

  const extent = 0.866 * ((cols + rows) / 2) * CELL + 0.866 * 2 * S;
  const vb = {
    x: -extent - 10,
    y: -0.5 * ((cols + rows) / 2) * CELL - BASE - amplitude - 34,
    w: 2 * extent + 20,
    h: ((cols + rows) / 2) * CELL + BASE + amplitude + 58,
  };

  useEffect(() => {
    const svg = svgRef.current;
    if (!svg) return;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const pointer = { x: 1e6, y: 1e6 }; // world coords, far away
    const onMove = (e: PointerEvent) => {
      const rect = svg.getBoundingClientRect();
      const sx = ((e.clientX - rect.left) / rect.width) * vb.w + vb.x;
      const sy = ((e.clientY - rect.top) / rect.height) * vb.h + vb.y;
      pointer.x = (sx / 0.866 + 2 * sy) / 2;
      pointer.y = (2 * sy - sx / 0.866) / 2;
    };
    const onLeave = () => {
      pointer.x = 1e6;
      pointer.y = 1e6;
    };
    svg.addEventListener("pointermove", onMove);
    svg.addEventListener("pointerleave", onLeave);

    let raf = 0;
    const draw = (t: number) => {
      for (const cell of cells) {
        const el = cellRefs.current[cell.idx];
        if (!el?.top || !el.right || !el.front) continue;
        const wave =
          (Math.sin(t * 0.0016 * speed + (cell.cx + cell.cy) * 0.024) + 1) *
          (amplitude / 2);
        const dx = cell.cx - pointer.x;
        const dy = cell.cy - pointer.y;
        const boost = Math.exp(-(dx * dx + dy * dy) / 2600) * amplitude * 1.6;
        const faces = cubeFaces(cell.cx, cell.cy, BASE + wave + boost);
        el.top.setAttribute("points", faces.top);
        el.right.setAttribute("points", faces.right);
        el.front.setAttribute("points", faces.front);
      }
    };

    if (reduced) {
      draw(0);
      return () => {
        svg.removeEventListener("pointermove", onMove);
        svg.removeEventListener("pointerleave", onLeave);
      };
    }

    const loop = (t: number) => {
      draw(t);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => {
      cancelAnimationFrame(raf);
      svg.removeEventListener("pointermove", onMove);
      svg.removeEventListener("pointerleave", onLeave);
    };
    // cells derived from rows/cols; vb from the same
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rows, cols, speed, amplitude]);

  return (
    <svg
      ref={svgRef}
      role="img"
      aria-label="Isometric cube grid with a rolling wave"
      viewBox={`${vb.x} ${vb.y} ${vb.w} ${vb.h}`}
      preserveAspectRatio="xMidYMid meet"
      className={["block h-auto w-full touch-none", className].filter(Boolean).join(" ")}
      style={{ maxWidth: size, aspectRatio: `${vb.w} / ${vb.h}`, ...style }}
    >
      {cells.map((cell) => {
        const faces = cubeFaces(cell.cx, cell.cy, BASE);
        return (
          <g key={cell.idx}>
            <polygon
              ref={(el) => {
                (cellRefs.current[cell.idx] ??= {
                  top: null,
                  right: null,
                  front: null,
                }).front = el;
              }}
              points={faces.front}
              fill={FRONT}
              stroke="var(--ink)"
              strokeOpacity={0.55}
              strokeWidth={1}
              vectorEffect="non-scaling-stroke"
              className="transition-[fill] duration-300"
            />
            <polygon
              ref={(el) => {
                (cellRefs.current[cell.idx] ??= {
                  top: null,
                  right: null,
                  front: null,
                }).right = el;
              }}
              points={faces.right}
              fill={RIGHT}
              stroke="var(--ink)"
              strokeOpacity={0.55}
              strokeWidth={1}
              vectorEffect="non-scaling-stroke"
              className="transition-[fill] duration-300"
            />
            <polygon
              ref={(el) => {
                (cellRefs.current[cell.idx] ??= {
                  top: null,
                  right: null,
                  front: null,
                }).top = el;
              }}
              points={faces.top}
              fill={TOP}
              stroke="var(--ink)"
              strokeOpacity={0.85}
              strokeWidth={1.4}
              vectorEffect="non-scaling-stroke"
              className="transition-[fill] duration-300"
            />
          </g>
        );
      })}
    </svg>
  );
}

export default IsometricCubes;

```

## Demo

```tsx
"use client";

import React from "react";
import { IsometricCubes } from "../mellow/isometric-cubes";

export default function IsometricCubesDemo() {
  return (
    <div className="flex flex-col items-center gap-4 p-6">
      <div className="w-full max-w-md rounded-2xl border border-[var(--rule)] bg-[rgba(var(--ink-rgb),0.02)] p-6">
        <IsometricCubes size={400} className="mx-auto w-full" />
        <div className="mt-2">
          <div className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.16em] text-[rgba(var(--ink-rgb),0.45)] uppercase">
            Compute
          </div>
          <div className="mt-1 [font-family:var(--font-sans)] text-base font-medium tracking-[-0.01em] text-[var(--ink)]">
            Elastic by default
          </div>
        </div>
      </div>
      <p className="text-[0.8125rem] text-[rgba(var(--ink-rgb),0.35)]">
        Move the cursor over the grid — nearby cubes rise
      </p>
    </div>
  );
}

```
