Reputation: 469
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
Upvotes: 2
Views: 89
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.
motion.div
- Motion Docsconst { 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
Reputation: 8845
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>
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