Code Talk: How the LEGO Effect Was Created on My Website

Cover Image for Code Talk: How the LEGO Effect Was Created on My Website
Mal Nushi
Mal Nushi

On the 'LEGO' page on my personal website, I wanted to add something to kind of resemble LEGO bricks breaking and building. To do this, I had to create a Typescript React component.

In this post, I want to go over how I made the component.

The Purpose

For every page on the 'sidequest' subpage on my website, I want to add something that is thematically relevant of the activity on the page. In this case, I needed to create a component that is in a way reminisicent of a LEGO's breaking and building.

Code

Here is the source code for the component:

/**
 * A LegoWordBuilder component.
 *
 * It's a client component that animates a word by breaking it apart and rebuilding it like LEGO blocks.
 */

"use client";

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

/**
 * Props for the LegoWordBuilder component.
 * @property {string} word - The word to animate like LEGO blocks
 * @property {number} [interval] - Controls full animation cycle time in ms (default: 5000ms)
 * @property {number} [buildTime] - Controls how long each transition takes in ms (default: 1000ms)
 * @property {string[]} [colors] - LEGO-like colors for letters
 * @property {string} [className] - Optional CSS class name for styling
 */
interface LegoWordBuilderProps {
  word: string;
  interval?: number;
  buildTime?: number;
  colors?: string[];
  className?: string;
}

/**
 * This renders a word that animates by breaking apart and rebuilding like LEGO blocks.
 * The animation cycle goes: built -> unbuilding -> unbuilt -> building -> built
 * @param {Object} props - Component props
 * @param {string} props.word - The word to animate
 * @param {number} props.interval - Full animation cycle duration
 * @param {number} props.buildTime - Transition duration
 * @param {string[]} props.colors - LEGO brick colors for letters
 * @param {string} props.className - Optional CSS class for styling
 * @returns {React.JSX.Element} - The rendered animated word
 */
export default function LegoWordBuilder({
  word,
  interval = 5000,
  buildTime = 1000,
  colors = ["#D01012", "#0D69AB", "#00852B", "#F8BB3D", "#F06D98"],
  className = "",
}: LegoWordBuilderProps): React.JSX.Element {
  // Animation states: built (assembled), unbuilding (breaking apart),
  // unbuilt (fully apart), building (coming back together)
  const [buildState, setBuildState] = useState<
    "built" | "unbuilding" | "unbuilt" | "building"
  >("built");

  useEffect(() => {
    // Animation cycle: built -> unbuilding -> unbuilt -> building -> built
    const cycle = () => {
      setBuildState("unbuilding");

      setTimeout(() => {
        setBuildState("unbuilt");

        setTimeout(() => {
          setBuildState("building");

          setTimeout(() => {
            setBuildState("built");
          }, buildTime);
        }, 800); // Pause in unbuilt state
      }, buildTime);
    };

    const timer = setInterval(cycle, interval);
    return () => clearInterval(timer);
  }, [interval, buildTime]);

  return (
    <span className={`inline-flex items-center align-baseline ${className}`}>
      {word.split("").map((letter, index) => (
        <span
          key={index}
          className={`lego-letter letter-${index}`}
          data-state={buildState}
          style={{
            color: colors[index % colors.length],
          }}
        >
          {letter}
        </span>
      ))}
      <style jsx>{`
        .lego-letter {
          position: relative;
          font-weight: bold;
          margin: 0 1px;
          display: inline-block;
          transition: all ${buildTime * 0.7}ms
            cubic-bezier(0.68, -0.55, 0.27, 1.55);
        }

        .lego-letter[data-state="built"] {
          transform: translateY(0) rotate(0);
          opacity: 1;
        }

        .lego-letter[data-state="unbuilding"] {
          opacity: 0.8;
        }

        .lego-letter[data-state="unbuilt"] {
          opacity: 0.6;
        }

        .lego-letter[data-state="building"] {
          opacity: 0.9;
        }

        ${Array.from({ length: word.length })
          .map(
            (_, i) => `
          .letter-${i}[data-state="unbuilding"] {
            transform: translateY(${i % 2 === 0 ? -15 : 15}px) rotate(${
              i % 2 === 0 ? -20 : 20
            }deg);
          }
          .letter-${i}[data-state="unbuilt"] {
            transform: translateY(${i % 2 === 0 ? -25 : 25}px) rotate(${
              i % 2 === 0 ? -40 : 40
            }deg);
          }
          .letter-${i}[data-state="building"] {
            transform: translateY(${i % 2 === 0 ? -5 : 5}px) rotate(${
              i % 2 === 0 ? -5 : 5
            }deg);
          }
        `
          )
          .join("")}
      `}</style>
    </span>
  );
}

The API

The props for the component are minimal by design. I did this to make the component reusable in any context where I would want to animate a word, not just for the 'build' as you see in the LEGO page example.

/**
 * Props for the LegoWordBuilder component.
 * @property {string} word - The word to animate like LEGO blocks
 * @property {number} [interval] - Controls full animation cycle time in ms (default: 5000ms)
 * @property {number} [buildTime] - Controls how long each transition takes in ms (default: 1000ms)
 * @property {string[]} [colors] - LEGO-like colors for letters
 * @property {string} [className] - Optional CSS class name for styling
 */
interface LegoWordBuilderProps {
  word: string;
  interval?: number;
  buildTime?: number;
  colors?: string[];
  className?: string;
}

Usage

In my website, I integrated the component on the LEGO page like this:

// Split the description string into an array of parts,
// separating out the word "build" (case-insensitive) as its own element.
// Example: "I want to build something" → ["I want to ", "build", " something"]
const parts = description.split(/(build)/i);

<h2 className="mb-8 text-neutral-600 dark:text-neutral-400">
  {parts.map((part, index) => {
    // Check if the current part is the target word "build" (case-insensitive)
    const isLegoWord = part.toLowerCase() === "build";

    // If it is, render it with the LegoWordBuilder animation
    // Otherwise, render it as a normal text span
    return isLegoWord ? (
      <LegoWordBuilder key={index} word={part} />
    ) : (
      <span key={index}>{part}</span>
    );
  })}
</h2>

The Animation

The word animation runs in a repeating 4 part cycle:

  1. built: The world is fully built.
  2. unbuilding: Letters rotate and scatter.
  3. unbuilt: Letters float in disarray.
  4. building: Letters come back together.

The transitions for the component are controlled using React's useState and useEffect. In each stage, there is a timeout set for the next, creating a smooth sequence:

useEffect(() => {
    // Animation cycle: built -> unbuilding -> unbuilt -> building -> built
    const cycle = () => {
      setBuildState("unbuilding");

      setTimeout(() => {
        setBuildState("unbuilt");

        setTimeout(() => {
          setBuildState("building");

          setTimeout(() => {
            setBuildState("built");
          }, buildTime);
        }, 800); // Pause in unbuilt state
      }, buildTime);
    };

    const timer = setInterval(cycle, interval);
    return () => clearInterval(timer);
  }, [interval, buildTime]);

CSS

The part I detest the most in web development, because for some reason my head just never wants to remember anything about CSS. Fortunately, I was able to get each letter individually animated using it! I assigned a class like .letter-0, .letter-1, etc., so that I can offset each individual letter and rotate them uniquely. This was done with CSS-in-JS. If the code was written in CSS, it would look something like this:

/* CSS render of the animation logic */
.letter-0[data-state="unbuilding"] {
  transform: translateY(-15px) rotate(-20deg);
}
.letter-0[data-state="unbuilt"] {
  transform: translateY(-25px) rotate(-40deg);
}
.letter-0[data-state="building"] {
  transform: translateY(-5px) rotate(-5deg);
}

I added this transition to give off a bouncy LEGO feel. Again this was also done in CSS-in-JS. If the code was written in pure CSS, it would look something like:

/* CSS render of the transition logic */
transition: all 700ms cubic-bezier(0.68, -0.55, 0.27, 1.55);

Component Design

I went with a monolithic approach for this component, but with some compositional-like ideas.

All animation logic, state management, individual letter rendering, and CSS lives in one file making it technically monolithic. Also there are no child components, no external helper functions, and no imported style modules. But, due to how I had to handle the individual letter mapping, build-state transitions, and customizable props, it has some influence of a compositional component. Definitely one of my more interesting components.