Sidath
Sidath

Reputation: 469

How to design rotating cube with text and image changing Hero section in Next.JS , Tailwind CSS and Framer Motion?

I am trying to re-create the string-sytems.co.uk (or you can see the GIF below) to learn the concepts of Next.js, Tailwind CSS, and Framer Motion.

I'm new to all these things and need to design the Hero Section first. There is a Header inside a cube that rotates and changes the text while rotating and changing the background images around the hero section.

You can try the code base here, what I tried: codesandbox.io example

Example GIF

enter image description here

Upvotes: 2

Views: 89

Answers (2)

rozsazoltan
rozsazoltan

Reputation: 8845

You can understand much more from the code if you tweak it a bit, but I’ll provide a few pointers. An essential setting for the 3D effect is the preserve-3d value for transformStyle.

After that, I defined the rotation circle's radius, then adjusted the height and width of the animated element accordingly. The rest is just formatting.

const { useEffect, useRef } = React;
const { motion } = Motion;

const blockSize = '64px'; // rotate radius
const width = `calc(${blockSize} * 6)`;
const duration = 2; // speed

function RotatingBlock() {
  return (
    // container
    React.createElement('div', { className: "flex items-center justify-center h-screen bg-gray-900" },
      // animated element
      React.createElement(motion.div, {
        className: "relative",
        animate: {
          rotateX: [0, -90, -180, -270, -360],
        },
        transition: {
          repeat: Infinity,
          duration: duration * 4,
          ease: "linear",
        },
        style: {
          transformStyle: "preserve-3d", // 3D style
          width: width,
          height: `calc(${blockSize} * 2)`,
        },
      },
        // 1.
        React.createElement('div', {
          className: "absolute w-full h-full bg-blue-500 flex items-center justify-center text-white text-lg font-bold",
          style: {
            transform: `rotateX(0deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "Side 1"),
        
        // 2.
        React.createElement('div', {
          className: "absolute w-full h-full bg-red-500 flex items-center justify-center text-white text-lg font-bold",
          style: {
            transform: `rotateX(90deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "Side 2"),
        
        // 3.
        React.createElement('div', {
          className: "absolute w-full h-full bg-green-500 flex items-center justify-center text-white text-lg font-bold",
          style: {
            transform: `rotateX(180deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "Side 3"),
        
        // 4.
        React.createElement('div', {
          className: "absolute w-full h-full bg-yellow-500 flex items-center justify-center text-white text-lg font-bold",
          style: {
            transform: `rotateX(270deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "Side 4"),
      )
    )
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(RotatingBlock));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/framer-motion.min.js"></script>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>

<div id="root"></div>

Specifically, to achieve your example, one thing missing is a slight delay between the two rotations. To speed up the process, I solved this with a trick; I've incorporated into the animation that it stays in one position for a longer time, as seen in the rotateX setting. Here, due to the rounding, I've "overlapped" the squares on top of each other so that no gap is visible at the rounded corners. However, this is only a nice solution if the sides of the rectangles have the same color.

const { useEffect, useRef } = React;
const { motion } = Motion;

const blockSize = '32px'; // rotate radius
const width = `32rem`;
const duration = 10; // speed

function RotatingBlock() {
  return (
    // container
    React.createElement('div', { className: "flex items-center justify-center h-screen bg-stone-50" }, 
      // animated element
      React.createElement(motion.div, {
        className: "relative",
        animate: {
          rotateX: [
            0, 0, 0, 0,
            -90, -90, -90, -90,
            -180, -180, -180, -180,
            -270, -270, -270, -270,
            -360,
          ],
        },
        transition: {
          repeat: Infinity,
          duration: duration,
          ease: "linear",
        },
        style: {
          transformStyle: "preserve-3d", // 3D style
          width: width,
          // At a 2x multiplier, the rectangles just touch each other, but due to the rounding, it creates an optical illusion as if I've "overlapped" them. If you don't color them blue, you'll see the trick.
          height: `calc(${blockSize} * 2.25)`,
        },
      },
        // 1.
        React.createElement('div', {
          className: "absolute w-full h-full bg-blue-500 flex items-center justify-center text-white text-4xl font-bold rounded-lg",
          style: {
            transform: `rotateX(0deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "cloud solutions"),
        
        // 2.
        React.createElement('div', {
          className: "absolute w-full h-full bg-blue-500 flex items-center justify-center text-white text-4xl font-bold rounded-lg",
          style: {
            transform: `rotateX(90deg) translateZ(${blockSize})`,
              backfaceVisibility: "hidden",
            },
        }, "enterprise-grade security"),
        
        // 3.
        React.createElement('div', {
          className: "absolute w-full h-full bg-blue-500 flex items-center justify-center text-white text-4xl font-bold rounded-lg",
          style: {
            transform: `rotateX(180deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "increased collaboration"),
        
        // 4.
        React.createElement('div', {
          className: "absolute w-full h-full bg-blue-500 flex items-center justify-center text-white text-4xl font-bold rounded-lg",
          style: {
            transform: `rotateX(270deg) translateZ(${blockSize})`,
            backfaceVisibility: "hidden",
          },
        }, "optimising processes"),
      )
    )
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(RotatingBlock));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/framer-motion.min.js"></script>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>

<div id="root"></div>

Upvotes: 1

rozsazoltan
rozsazoltan

Reputation: 8845

Reproduce from original code

I'm not going to translate it to React and start rewriting it, but it could be a perfect starting point to find and review the original code. This way, it might be easier for you to achieve your goal.

// Source: https://www.string-systems.co.uk/wp-content/themes/string-systems/js/dist/main.min.js?ver=1730854785

var homepageHeader = document.querySelector(".pageheader--home"),
  cubeInit = function (a) {
    if (a) {
      var b = a.querySelector(".pageheader__cube"),
        c = b.querySelectorAll(".cube__face"),
        e = 0;
      setInterval(function () {
        e === c.length - 1 ? (e = 0) : e++;
        var a = 0 === e ? c[c.length - 1] : c[e - 1],
          b = c[e],
          f = e === c.length - 1 ? c[0] : c[e + 1];
        c.forEach(function (a) {
          a.classList.remove("cube__face--active"),
            a.classList.remove("cube__face--previous"),
            a.classList.remove("cube__face--next");
        }),
          b.classList.add("cube__face--active"),
          a.classList.add("cube__face--previous"),
          f.classList.add("cube__face--next");
      }, 2e3);
    }
  };
cubeInit(homepageHeader);
* {
  font-size: 40px;
}

/* Source: https://www.string-systems.co.uk/wp-content/cache/min/1/wp-content/themes/string-systems/style.min.css?ver=1731497502 */

.pageheader--home .pageheader__scene {
  width: 100%;
  height: 1.5em;
  margin: .15em auto;
  -webkit-perspective: calc(1.5em * 2);
  perspective: calc(1.5em * 2)
}

.pageheader--home .pageheader__scene .pageheader__cube {
  width: 100%;
  height: 1.5em;
  position: relative;
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
  -webkit-transform: translateZ(calc(-1.5em / 2));
  transform: translateZ(calc(-1.5em / 2))
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face {
  position: absolute;
  overflow: hidden;
  width: 100%;
  height: 1.5em;
  padding: 0 .25em;
  box-sizing: border-box;
  background-color: #3066f2;
  line-height: 1.5em;
  color: #fff;
  text-align: center;
  border-radius: .55rem;
  -webkit-transform: rotateX(180deg) translateZ(calc(1.5em / 2));
  transform: rotateX(180deg) translateZ(calc(1.5em / 2))
}

@media only screen and (min-width: 700px) {
  .pageheader--home .pageheader__scene .pageheader__cube .cube__face {
      border-radius:.8rem
  }
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--next {
  -webkit-transform: rotateX(90deg) translateZ(calc(1.5em / 2));
  transform: rotateX(90deg) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--active {
  -webkit-transform: rotateX(0) translateZ(calc(1.5em / 2));
  transform: rotateX(0) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--previous {
  -webkit-transform: rotateX(-90deg) translateZ(calc(1.5em / 2));
  transform: rotateX(-90deg) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}
<!-- Source: https://www.string-systems.co.uk/ -->

<div class="pageheader--home">
  <div data-nosnippet="" class="pageheader__scene">
    <div class="pageheader__cube">
      <div class="cube__face cube__face--active">cloud solutions</div>
      <div class="cube__face cube__face--next">enterprise-grade security</div>
      <div class="cube__face">increased collaboration </div>
      <div class="cube__face">optimising processes</div>
      <div class="cube__face cube__face--previous">improved performance</div>
    </div>
  </div>
</div>

Refactor

Can make the minified JS more readable for interpretation.

// Function to initialize the cube animation
const initCubeAnimation = (homepageHeader) => {
  if (!homepageHeader) {
    console.error("Homepage header not found.");
    return;
  }

  // Find the cube and its faces
  const cube = homepageHeader.querySelector(".pageheader__cube");
  const cubeFaces = cube ? cube.querySelectorAll(".cube__face") : [];

  if (cubeFaces.length === 0) {
    console.error("Cube faces not found.");
    return;
  }

  let currentIndex = 0;

  // Function to update the cube faces' classes for the animation
  const updateCubeFaces = () => {
    // Get the previous, current, and next faces based on the current index
    const previousIndex = currentIndex === 0 ? cubeFaces.length - 1 : currentIndex - 1;
    const nextIndex = currentIndex === cubeFaces.length - 1 ? 0 : currentIndex + 1;

    // Remove active, previous, and next states from all cube faces
    cubeFaces.forEach(face => {
      face.classList.remove("cube__face--active", "cube__face--previous", "cube__face--next");
    });

    // Add the appropriate classes for the current, previous, and next cube faces
    cubeFaces[currentIndex].classList.add("cube__face--active");
    cubeFaces[previousIndex].classList.add("cube__face--previous");
    cubeFaces[nextIndex].classList.add("cube__face--next");
  };

  // Update the cube faces every 2 seconds
  setInterval(() => {
    // Increment the index, and loop back to 0 if we reach the end
    currentIndex = (currentIndex + 1) % cubeFaces.length;
    updateCubeFaces();
  }, 2000); // Change the interval time if needed
};

// Initialize the cube animation on the homepage header
const homepageHeader = document.querySelector(".pageheader--home");
initCubeAnimation(homepageHeader);
* {
  font-size: 40px;
}

/* Source: https://www.string-systems.co.uk/wp-content/cache/min/1/wp-content/themes/string-systems/style.min.css?ver=1731497502 */

.pageheader--home .pageheader__scene {
  width: 100%;
  height: 1.5em;
  margin: .15em auto;
  -webkit-perspective: calc(1.5em * 2);
  perspective: calc(1.5em * 2)
}

.pageheader--home .pageheader__scene .pageheader__cube {
  width: 100%;
  height: 1.5em;
  position: relative;
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
  -webkit-transform: translateZ(calc(-1.5em / 2));
  transform: translateZ(calc(-1.5em / 2))
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face {
  position: absolute;
  overflow: hidden;
  width: 100%;
  height: 1.5em;
  padding: 0 .25em;
  box-sizing: border-box;
  background-color: #3066f2;
  line-height: 1.5em;
  color: #fff;
  text-align: center;
  border-radius: .55rem;
  -webkit-transform: rotateX(180deg) translateZ(calc(1.5em / 2));
  transform: rotateX(180deg) translateZ(calc(1.5em / 2))
}

@media only screen and (min-width: 700px) {
  .pageheader--home .pageheader__scene .pageheader__cube .cube__face {
      border-radius:.8rem
  }
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--next {
  -webkit-transform: rotateX(90deg) translateZ(calc(1.5em / 2));
  transform: rotateX(90deg) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--active {
  -webkit-transform: rotateX(0) translateZ(calc(1.5em / 2));
  transform: rotateX(0) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}

.pageheader--home .pageheader__scene .pageheader__cube .cube__face--previous {
  -webkit-transform: rotateX(-90deg) translateZ(calc(1.5em / 2));
  transform: rotateX(-90deg) translateZ(calc(1.5em / 2));
  -webkit-transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: -webkit-transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1);
  transition: transform 1s cubic-bezier(.65,0,.35,1),-webkit-transform 1s cubic-bezier(.65,0,.35,1)
}
<!-- Source: https://www.string-systems.co.uk/ -->

<div class="pageheader--home">
  <div data-nosnippet="" class="pageheader__scene">
    <div class="pageheader__cube">
      <div class="cube__face cube__face--active">cloud solutions</div>
      <div class="cube__face cube__face--next">enterprise-grade security</div>
      <div class="cube__face">increased collaboration </div>
      <div class="cube__face">optimising processes</div>
      <div class="cube__face cube__face--previous">improved performance</div>
    </div>
  </div>
</div>

Upvotes: 1

Related Questions