Docs

Led Board

A component that mimicks LED board and can display words; as seen in NextJS's homepage. Hover over the component to see the animation.

requires interactionhover

Installation

Update tailwind.config.js

Add the following to your tailwind.config.js file.

module.exports = {
  theme: {
    extend: {
      keyframes: {
        led: {
          "0%": { fill: "currentColor", brightness: "1" },
          "50%": { fill: "#a855f7", brightness: "500%" },
          "100%": { fill: "currentColor", brightness: "1" },
        },
      },
      animation: {
        led: "led 100ms ease-in-out",
      },
    }
  }
}

Run the following command

It will create a new file led-board.tsx inside the components/animata/card directory.

mkdir -p components/animata/card && touch components/animata/card/led-board.tsx

Paste the code

Open the newly created file and paste the following code:

import React, { useEffect, useState } from "react";
 
import { cn } from "@/lib/utils";
 
const charMap: Record<string, number[][]> = {
  C: [
    [0, 1, 1, 1],
    [1, 0, 0, 0],
    [1, 0, 0, 0],
    [1, 0, 0, 0],
    [1, 0, 0, 0],
    [0, 1, 1, 1],
  ],
  O: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0],
  ],
  P: [
    [1, 1, 1, 0],
    [1, 0, 0, 1],
    [1, 0, 0, 1],
    [1, 1, 1, 0],
    [1, 0, 0, 0],
    [1, 0, 0, 0],
  ],
  Y: [
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
  ],
  " ": [
    [0, 0],
    [0, 0],
    [0, 0],
    [0, 0],
    [0, 0],
    [0, 0],
  ],
  0: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0],
  ],
  1: [
    [0, 0, 1],
    [0, 1, 1],
    [1, 0, 1],
    [0, 0, 1],
    [0, 0, 1],
    [0, 0, 1],
  ],
  3: [
    [1, 1, 1, 1, 0],
    [0, 0, 0, 0, 1],
    [0, 0, 1, 1, 0],
    [0, 0, 1, 1, 0],
    [0, 0, 0, 0, 1],
    [1, 1, 1, 1, 0],
  ],
  2: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 1, 0],
    [0, 0, 1, 0, 0],
    [0, 1, 0, 0, 0],
    [1, 1, 1, 1, 1],
  ],
  4: [
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 1],
    [0, 0, 0, 0, 1],
    [0, 0, 0, 0, 1],
  ],
  5: [
    [1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0],
    [1, 1, 1, 1, 0],
    [0, 0, 0, 0, 1],
    [0, 0, 0, 0, 1],
    [1, 1, 1, 1, 0],
  ],
  6: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 0],
    [1, 0, 0, 0, 0],
    [1, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0],
  ],
  7: [
    [1, 1, 1, 1, 1],
    [0, 0, 0, 0, 1],
    [0, 0, 0, 1, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
  ],
  8: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0],
  ],
  9: [
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 1],
    [0, 0, 0, 0, 1],
    [0, 0, 0, 0, 1],
  ],
};
 
function createMatrix(rows: number, cols: number) {
  return Array(rows)
    .fill("")
    .map(() => Array(cols).fill(false));
}
 
interface Board {
  rows: number;
  cols: number;
  matrix: boolean[][];
}
 
function createBoard(word: string) {
  const rows = 15;
  const wordArray = word.trim().toUpperCase().split("");
  const cols =
    wordArray
      // +1 for extra padding
      .map((char) => (charMap[char] || charMap[" "])[0].length + 1)
      .reduce((a, b) => a + b, 1) * 2;
 
  const matrix = createMatrix(rows, cols);
  const startRow = 2;
  let startCol = 2;
 
  for (const charIndex in wordArray) {
    const char = wordArray[charIndex];
    const charPattern = charMap[char] || charMap[" "];
 
    for (const rowIndex in charPattern) {
      const row = charPattern[rowIndex];
      for (const colIndex in row) {
        const isLit = row[colIndex];
        if (isLit) {
          matrix[startRow + +rowIndex * 2][startCol + +colIndex * 2] = true;
        }
      }
    }
 
    // +1 for extra spacing
    startCol += (charPattern[0].length + 1) * 2;
  }
 
  return {
    rows,
    cols,
    matrix,
  };
}
 
export default function LEDBoard({
  word = "COPY",
}: {
  /**
   * The word to display on the LED board.
   * Currently only supports "C", "O","P", " and "Y". But you can add more in the `charMap` object.
   */
  word: string;
}) {
  const [{ rows, cols, matrix }, setBoard] = useState<Board>(createBoard(word));
 
  useEffect(() => setBoard(createBoard(word)), [word]);
 
  const [isHovering, setIsHovering] = useState(false);
  const [, setForceUpdate] = useState(0);
 
  useEffect(() => {
    if (isHovering) {
      return;
    }
 
    const interval = setInterval(() => {
      // Force a re-render so the random dots are animated
      setForceUpdate((current) => current + 1);
      // max animation duration is 3000ms (2000ms + 1000ms)
    }, 3000);
 
    return () => clearInterval(interval);
  }, [isHovering]);
 
  return (
    <div
      className="group rounded-xl border border-gray-600 bg-gradient-to-bl from-zinc-950/80 via-zinc-900 via-30% to-zinc-950 to-75% p-4 dark:border-zinc-800"
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      <svg className="h-auto w-full text-zinc-800" viewBox={`0 0 ${cols - 1} ${rows}`}>
        {matrix.map((row, rowIndex) =>
          row.map((isLit, colIndex) => {
            // Hide all odd rows and columns
            if (rowIndex % 2 === 1 || colIndex % 2 === 1) {
              return null;
            }
 
            const shouldAnimate = !isHovering && isLit && Math.random() > 0.8;
            let delay = 0;
            if (shouldAnimate) {
              delay = Math.floor(Math.random() * 1000);
            }
 
            return (
              <circle
                key={`${rowIndex}-${colIndex}`}
                cx={colIndex + 0.25}
                cy={rowIndex + 0.25}
                r={0.25}
                style={{
                  transitionDelay: !isHovering ? `${colIndex * 15}ms` : "0ms",
                  animationDuration: "2000ms",
                  animationDelay: `${delay}ms`,
                }}
                className={cn("fill-zinc-800 transition-all duration-200 ease-in-out", {
                  "group-hover:fill-purple-500": isLit,
                  "animate-led ease-in-out": shouldAnimate,
                })}
              />
            );
          }),
        )}
      </svg>
    </div>
  );
}

Credits

Built by hari

Inspired by NextJS's homepage