dqnamo

Game of Life Loader

Component 127

About

A tiny React canvas component for ambient loading states. Renders a softly fading Conway's Game of Life simulation behind your content — deterministic seeding, reduced-motion support, theme-aware color, and pause-when-hidden. Fully typed props for cell size, density, fade, and step interval.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Game of Life Loader to your project and fits it to your theme.

Code

How Game of Life Loader is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { useEffect, useRef, useState } from "react";
import { GameOfLifeLoader } from "@/components/ui/game-of-life-loader";

const W = 520;
const H = 170;
// Exact geometry from the source (520×170 viewBox).
const ORGANIC_Y = [
  68.09, 64.13, 42.17, 125.63, 30.31, 79.95, 6.59, 20.21, 0, 73.36, 6.59, 28.99,
];
const SPONSORED_Y = [
  170, 170, 170, 170, 170, 170, 170, 170, 117.29, 170, 170, 170,
];
const LOADING_MESSAGES = [
  "Mapping follower signals...",
  "Preparing chart data...",
  "Loading events...",
];

function points(ys: number[]) {
  const step = W / (ys.length - 1);
  return ys.map((y, index) => [index * step, y] as const);
}

function linePath(ys: number[]) {
  return points(ys)
    .map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`)
    .join(" ");
}

function areaPath(ys: number[]) {
  const line = points(ys)
    .map(([x, y]) => `L ${x.toFixed(2)} ${y.toFixed(2)}`)
    .join(" ");
  return `M ${line.slice(2)} L ${W} ${H} L 0 ${H} Z`;
}

function FollowerChart({ resolved }: { resolved: boolean }) {
  return (
    <svg
      aria-hidden="true"
      className="absolute inset-x-0 top-0 bottom-6 h-auto w-full overflow-visible"
      preserveAspectRatio="none"
      viewBox={`0 0 ${W} ${H}`}
    >
      <defs>
        <linearGradient id="organic-area" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor="rgb(59 130 246)" stopOpacity="0.2" />
          <stop offset="100%" stopColor="rgb(59 130 246)" stopOpacity="0" />
        </linearGradient>
        <linearGradient id="sponsored-area" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor="rgb(139 92 246)" stopOpacity="0.16" />
          <stop offset="100%" stopColor="rgb(139 92 246)" stopOpacity="0" />
        </linearGradient>
      </defs>
      <path
        d={areaPath(ORGANIC_Y)}
        fill="url(#organic-area)"
        style={{ opacity: resolved ? 1 : 0, transition: "opacity 700ms ease-out 180ms" }}
      />
      <path
        d={areaPath(SPONSORED_Y)}
        fill="url(#sponsored-area)"
        style={{ opacity: resolved ? 1 : 0, transition: "opacity 700ms ease-out 280ms" }}
      />
      <path
        d={linePath(ORGANIC_Y)}
        fill="none"
        pathLength={1}
        stroke="rgb(59 130 246)"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth={2}
        style={{
          strokeDasharray: 1,
          strokeDashoffset: resolved ? 0 : 1,
          opacity: resolved ? 1 : 0,
          transition:
            "stroke-dashoffset 900ms cubic-bezier(0.22, 1, 0.36, 1), opacity 300ms ease-out",
        }}
        vectorEffect="non-scaling-stroke"
      />
      <path
        d={linePath(SPONSORED_Y)}
        fill="none"
        pathLength={1}
        stroke="rgb(139 92 246)"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth={2}
        style={{
          strokeDasharray: 1,
          strokeDashoffset: resolved ? 0 : 1,
          opacity: resolved ? 1 : 0,
          transition:
            "stroke-dashoffset 900ms cubic-bezier(0.22, 1, 0.36, 1) 120ms, opacity 300ms ease-out 120ms",
        }}
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}

// One rotating loading message that slides + fades in on mount.
function LoadingMessage({ text }: { text: string }) {
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const frame = requestAnimationFrame(() => setShown(true));
    return () => cancelAnimationFrame(frame);
  }, []);
  return (
    <span
      className="inline-block transition-[translate,opacity] duration-300 ease-out"
      style={{
        opacity: shown ? 1 : 0,
        transform: shown ? "translateY(0)" : "translateY(8px)",
      }}
    >
      {text}
    </span>
  );
}

export default function GameOfLifeLoaderDemo() {
  const [resolved, setResolved] = useState(false);
  const [messageIndex, setMessageIndex] = useState(0);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Keep the empty wait active, then resolve into the graph once "data is ready".
  useEffect(() => {
    if (resolved) {
      return;
    }
    timer.current = setTimeout(() => setResolved(true), 2600);
    return () => {
      if (timer.current) {
        clearTimeout(timer.current);
      }
    };
  }, [resolved]);

  // Rotate the loading messages while waiting.
  useEffect(() => {
    if (resolved) {
      return;
    }
    const id = setInterval(
      () => setMessageIndex((index) => (index + 1) % LOADING_MESSAGES.length),
      1100,
    );
    return () => clearInterval(id);
  }, [resolved]);

  function replay() {
    if (timer.current) {
      clearTimeout(timer.current);
    }
    setMessageIndex(0);
    setResolved(false);
  }

  return (
    <div className="flex min-h-screen w-full items-center justify-center bg-neutral-50 p-8">
      <div className="flex w-full max-w-md flex-col gap-3">
        <div className="relative h-[22rem] w-full overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm">
          {/* resolved graph — z-10, below the loader */}
          <div className="absolute inset-0 z-10 p-6">
            <h3 className="font-medium text-neutral-900 text-sm">
              Follower metrics
            </h3>
            <div className="relative mt-5 h-44">
              <div className="absolute inset-x-0 top-0 bottom-6 grid grid-rows-4">
                <div className="border-t border-neutral-200" />
                <div className="border-t border-neutral-200" />
                <div className="border-t border-neutral-200" />
                <div className="border-t border-neutral-200" />
              </div>
              <FollowerChart resolved={resolved} />
              <div
                className="absolute inset-x-0 bottom-0 flex justify-between font-mono text-[10px] text-neutral-400 uppercase leading-none transition-all duration-500 ease-out"
                style={{
                  opacity: resolved ? 1 : 0,
                  transform: resolved ? "translateY(0)" : "translateY(8px)",
                }}
              >
                <span>Jan 23</span>
                <span>Dec 23</span>
              </div>
            </div>
            <div className="mt-2 divide-y divide-neutral-200">
              <div
                className="flex items-center justify-between py-2 text-neutral-500 text-sm transition-all duration-500 ease-out"
                style={{
                  opacity: resolved ? 1 : 0,
                  transform: resolved ? "translateY(0)" : "translateY(8px)",
                  transitionDelay: "520ms",
                }}
              >
                <div className="flex items-center gap-2">
                  <span aria-hidden="true" className="h-0.5 w-3 bg-blue-500" />
                  <span>Organic</span>
                </div>
                <span className="font-medium text-neutral-900">3,273</span>
              </div>
              <div
                className="flex items-center justify-between py-2 text-neutral-500 text-sm transition-all duration-500 ease-out"
                style={{
                  opacity: resolved ? 1 : 0,
                  transform: resolved ? "translateY(0)" : "translateY(8px)",
                  transitionDelay: "610ms",
                }}
              >
                <div className="flex items-center gap-2">
                  <span aria-hidden="true" className="h-0.5 w-3 bg-violet-500" />
                  <span>Sponsored</span>
                </div>
                <span className="font-medium text-neutral-900">120</span>
              </div>
            </div>
          </div>

          {/* loading state — z-20, crossfades out to reveal the graph */}
          <div
            className="absolute inset-0 z-20 overflow-hidden bg-white p-6 transition-all duration-500 ease-out"
            style={{
              opacity: resolved ? 0 : 1,
              transform: resolved ? "translateY(-8px)" : "translateY(0)",
              pointerEvents: resolved ? "none" : "auto",
            }}
          >
            <GameOfLifeLoader
              cellColor="rgb(100 116 139)"
              cellRadius={3}
              cellSize={14}
              density={0.28}
              fadeDuration={920}
              maxOpacity={0.22}
              stepInterval={620}
            />
            <div className="absolute inset-0 z-10 flex flex-col items-center justify-center p-6 text-center">
              <p
                aria-live="polite"
                className="flex h-7 items-center overflow-hidden font-medium text-neutral-500 text-sm leading-5"
              >
                <LoadingMessage
                  key={messageIndex}
                  text={LOADING_MESSAGES[messageIndex]}
                />
              </p>
            </div>
          </div>
        </div>

        <div className="flex items-center justify-between rounded-xl border border-neutral-200 bg-white px-4 py-3">
          <div className="min-w-0">
            <p className="font-medium text-neutral-900 text-sm">
              Animated loading state
            </p>
            <p className="text-neutral-500 text-xs">
              A live loading state resolves into follower metrics.
            </p>
          </div>
          <button
            className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-neutral-200 bg-white px-2.5 py-1.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50"
            onClick={replay}
            type="button"
          >
            <svg
              aria-hidden="true"
              className="size-3.5"
              fill="none"
              stroke="currentColor"
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              viewBox="0 0 24 24"
            >
              <path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6" />
            </svg>
            Replay
          </button>
        </div>
      </div>
    </div>
  );
}

More by dqnamo

View all
  • Agent Dock

    Agent Dock

    538
  • Animated Banner

    Animated Banner

    335
  • Scroll Fade List

    Scroll Fade List

    298
  • Model Selector Prompt

    Model Selector Prompt

    154
  • Animated Signature

    Animated Signature

    55
  • Iridescent Foil

    Iridescent Foil

    52
  • Dynamic Button

    Dynamic Button

    44
  • Scramble Text

    Scramble Text

    32