I am trying to build a website through framer ( a no code tool ). I wanted a text opacity to change on scroll but section to remain sticky once the user scrolls to the section. This section should remain sticky till the very last character of the text. Also the animation should kick in only when the text section appears in the middle of the viewport and not before that. Attaching source code for your kind consideration.
PS – I am not a coder. I am looking for bread and butter solution.
import * as React from "react";
import { useEffect, useRef, useState } from "react";
import { motion, useAnimation } from "framer-motion";
import { addPropertyControls, ControlType } from "framer";
import { useInView } from "react-intersection-observer";
export function TextOpacityOnScroll(props) {
const {
text,
fontFamily,
fontSize,
fontWeight,
lineHeight,
color,
letterSpacing,
textAlign,
padding,
borderRadius,
sticky,
topOffset,
} = props;
const controls = useAnimation();
const [ref, inView] = useInView({
triggerOnce: false,
threshold: 0.1,
});
const [isScrolling, setIsScrolling] = useState(false);
const [scrollDirection, setScrollDirection] = useState("down");
const [animationComplete, setAnimationComplete] = useState(false);
const prevScrollY = useRef(window.scrollY);
const animationProgress = useRef(0);
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY > prevScrollY.current) {
setScrollDirection("down");
} else {
setScrollDirection("up");
}
prevScrollY.current = currentScrollY;
setIsScrolling(true);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
useEffect(() => {
const handleStopScroll = () => {
setIsScrolling(false);
};
const scrollStopListener = () => {
clearTimeout(handleStopScroll);
setTimeout(handleStopScroll, 150);
};
window.addEventListener("scroll", scrollStopListener);
return () => {
window.removeEventListener("scroll", scrollStopListener);
};
}, []);
useEffect(() => {
if (inView && isScrolling && !animationComplete) {
const animate = async () => {
if (scrollDirection === "down") {
animationProgress.current += 2; // Smooth and faster animation
} else {
animationProgress.current -= 2; // Smooth and faster animation
}
animationProgress.current = Math.max(0, Math.min(animationProgress.current, text.length));
if (animationProgress.current === text.length) {
setAnimationComplete(true);
} else {
setAnimationComplete(false);
}
const characterAnimations = text.split("").map((char, index) => (
<motion.span
key={index}
initial={{ opacity: 0.2 }}
animate={controls}
custom={index}
variants={{
visible: (i) => ({
opacity: i <= animationProgress.current ? 1 : 0.2,
transition: { duration: 0.3 },
}),
hidden: (i) => ({
opacity: i > animationProgress.current ? 0.2 : 1,
transition: { duration: 0.3 },
}),
}}
>
{char}
</motion.span>
));
await controls.start("visible");
return characterAnimations;
};
animate();
}
}, [inView, isScrolling, scrollDirection, controls, text, animationComplete]);
const containerStyle = {
padding: `${padding}px`,
borderRadius: `${borderRadius}px`,
fontFamily,
fontSize: `${fontSize}px`,
fontWeight,
lineHeight,
letterSpacing,
textAlign,
position: "sticky",
top: `${topOffset}px`,
zIndex: 1, // Ensure it stays on top while sticky
};
return (
<div ref={ref} style={containerStyle}>
{text.split("").map((char, index) => (
<motion.span
key={index}
initial={{ opacity: 0.2 }}
animate={controls}
custom={index}
variants={{
visible: (i) => ({
opacity: i <= animationProgress.current ? 1 : 0.2,
transition: { duration: 0.3 },
}),
hidden: (i) => ({
opacity: i > animationProgress.current ? 0.2 : 1,
transition: { duration: 0.3 },
}),
}}
>
{char}
</motion.span>
))}
</div>
);
}
TextOpacityOnScroll.defaultProps = {
text: "Sample Text",
fontFamily: "Inter",
fontSize: 16,
fontWeight: "400",
lineHeight: "1.5em",
color: "#303030",
letterSpacing: "normal",
textAlign: "left",
padding: 10,
borderRadius: 5,
sticky: true,
topOffset: 0,
};
addPropertyControls(TextOpacityOnScroll, {
text: { type: ControlType.String, title: "Text" },
fontFamily: { type: ControlType.String, title: "Font Family", defaultValue: "Inter" },
fontSize: { type: ControlType.Number, title: "Font Size", defaultValue: 16, min: 10, max: 100, step: 1 },
fontWeight: { type: ControlType.String, title: "Font Weight", defaultValue: "400" },
lineHeight: { type: ControlType.String, title: "Line Height", defaultValue: "1.5em" },
color: { type: ControlType.Color, title: "Text Color", defaultValue: "#303030" },
letterSpacing: { type: ControlType.String, title: "Letter Spacing", defaultValue: "normal" },
textAlign: {
type: ControlType.Enum,
title: "Text Align",
defaultValue: "left",
options: ["left", "center", "right"],
optionTitles: ["Left", "Center", "Right"],
},
padding: { type: ControlType.Number, title: "Padding", defaultValue: 10, min: 0, max: 100, step: 1 },
borderRadius: { type: ControlType.Number, title: "Border Radius", defaultValue: 5, min: 0, max: 50, step: 1 },
sticky: {
type: ControlType.Boolean,
title: "Sticky",
defaultValue: true,
enabledTitle: "Yes",
disabledTitle: "No",
},
topOffset: { type: ControlType.Number, title: "Top Offset", defaultValue: 0, min: 0, max: 100, step: 1 },
});
I tried chatGPT. But here’s the problem
- the section does not remain sticky long enough for the animation to be completed
- Also, the animation starts early even before the section comes into view.
I wanted the text to animate when the text comes right in the middle of the view port and remain sticky until the very last character gets animated
Arvind is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.