`const pinLinks = gsap.utils.toArray('.pinScroll > div');
pinLinks.forEach((pinLink, index) => {
const lastpinLinks = document.querySelector('.pinScroll > div:last-child');
ScrollTrigger.create({
trigger: pinLink,
start: 'top center',
end: () => `+=${pinLink.clientHeight}`,
toggleActions: 'play reverse none reverse',
toggleClass: { targets: pinLink, className: "active"},
});
});`
The above code works fine for me to toggleClass based on scroll, but it removes the active class from the last child, which I don't want.
Can you please tell me how can is keep active class on last child?
Related
I have a list (ParentBox.tsx) that contains many items (Box.tsx). When clicking the Add button, the ParentBox has one additional unique Box. The animation works fine. However, there are two scenarios where it does not:
When I click on the Box, it removes the item from the list. Framer Motion removes the Box from the user interface without exit animation.
When clicking "Remove All", the whole list of items is removed. There is no exit stagger effect.
I want to have an individual element of the list animated out, and when the whole list is cleared, have them one by one animated out.
Full Repro in CodeSanbox
Parent Box
const variantsBoxContainer: Variants = {
hidden: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: -1
}
},
show: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: 1
}
}
};
let id = 3;
export const ParentBox = (props: ParentBoxProps) => {
const [items, setItems] = useState<Item[]>([
{ id: 1, text: "Test #1" },
{ id: 2, text: "Test #2" }
]);
return (
<motion.div
className="parentbox"
>
<button
onClick={() => {
id++;
setItems([...items, { id: id, text: `Click to delete id ${id}` }]);
}}
>
Add
</button>
<button
onClick={() => {
id++;
setItems([]);
}}
>
Remove All
</button>
<motion.ol
variants={variantsBoxContainer}
initial="hidden"
animate="show"
exit="hidden"
>
<AnimatePresence mode="popLayout">
{items
.sort((a, b) => a.id - b.id)
.map((d) => (
<Box
key={d.id}
data={d}
onRemove={(item) => {
const newList = items.filter((i) => i.id !== item.id);
console.log(newList);
setItems(newList);
}}
/>
))}
</AnimatePresence>
</motion.ol>
</motion.div>
);
};
Box
const variantBox: Variants = {
hidden: { opacity: 0, top: -100, transition: { duration: 2 } },
show: { opacity: 1, top: 0, transition: { duration: 2 } }
};
export const Box = (props: BoxProps) => {
return (
<motion.li
className="box"
variants={variantBox}
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
What I have tried so far:
Adding/Removing the explicit mention of initial, animate, exit on the Box component.
Adding/Removing the when option.
Tried all mode in the AnimatedPresence
Try to add a function for the hidden (exit) variant to have a custom delay per index
Ensure all Box all have unique key
Let me know if you have any idea what I am missing to have the animation on Box removal (children).
CodeSanbox
Exit animations will work if you explicitly indicate which variant to use for the animation states:
export const Box = (props: BoxProps) => {
return (
<motion.li
custom={props.index}
className="box"
variants={variantBox}
exit="hidden"
initial="hidden"
animate="show"
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
I believe AnimatePresence is conflicting with the staggerChildren prop since it appears between the parent and children. See this issue on GitHub.
Quickest workaround is probably to use dynamic variants and manually set a delay in the variants for the Box component (based on the index in the items array.
The issue is like this.
When reloaded, the first step and pop-up show in the upper left corner instead of the center, this is the first problem for me. I expected it to position center as the Intro.js official document says.
Next, when you press "Next," the pop-up bites into the upper left corner and we can't see it. This is the second problem.
Furthermore, when "Next" is pressed, the pop-up also bites into the upper left corner and we can't see it. This is the second problem as well.
But furthermore, when "Next" is pressed, the pop-up appears in its normal expected position.
When you press "Next" again, the pop-up will still appear in the normal expected position.
The code looks like this.
import dynamic from 'next/dynamic';
import { useState } from 'react';
// Intro.js, see the details here: https://introjs.com/
// Intro.js-react, see the details here: https://github.com/HiDeoo/intro.js-react
// #ts-ignore
const Steps = dynamic(() => import('intro.js-react').then((mod) => mod.Steps), {
ssr: false
});
const Onboarding = () => {
const [stepEnabled, setStepEnabled] = useState(true);
const steps = [
{
title: 'Welcome!!',
intro:
'This is your dashboard. Once you have set up, numbers will be displayed.'
},
{
element: '#user-settings',
title: 'User Settings page',
intro: 'You can jump to the User Settings page from here.',
position: 'right'
},
{
element: '#profile-list',
intro: 'This is your profile list.',
position: 'right'
},
{
element: '#card-list',
intro: 'This is your card list.',
position: 'left'
}
];
const onExit = () => {
setStepEnabled(true);
};
const options = {
showProgress: true,
showBullets: true,
exitOnOverlayClick: true,
exitOnEsc: true,
nextLabel: 'Next',
prevLabel: 'Prev',
// skipLabel: 'Skip',
hidePrev: true,
doneLabel: 'Done',
overlayOpacity: 0.5,
overlayColor: '#000',
showStepNumbers: true,
keyboardNavigation: true,
scrollToElement: true,
helperElementPadding: 10,
showButtons: true
};
// if (!stepEnabled) {
// return null;
// }
return (
<Steps
// #ts-ignore
enabled={stepEnabled}
steps={steps}
initialStep={0}
onExit={onExit}
options={options}
/>
);
};
export default Onboarding;
Does anyone know why and how to fix it?
you can give customized css class to every object in intro.js like this :
()
{
element: ".test",
intro: "",
tooltipClass: "cssClassName1",
tooltipPosition: "bottom-center",
},
I am trying to imply some animations in an image while scrolling , the image will grow while scrolling down and return to normal when scrolling up . The reference : codepen.
This is my react code :
<ArticleWrapper ref={ref}>
<img id='grow' className='image' src={Img} alt='Image' />
</ArticleWrapper>
I have tried to implement this but it didnt work when I scroll :
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
scrollTrigger: {
trigger: "#grow",
scrub: 1.5,
start: "top center",
end: "+=400",
ease: "power1.out"
},
{
duration: 1,
scale: 1
}
);
}, []);
Any idea how to do that ?
I've noticed some strange behavior of GSAP scrolling in GatsbyJS. I have a container with full-page sections that are scrolled with GSAP.
I open a tab in a browser and go to my website. The scrolling works fine. Then I open a new tab and go to (let's say) google.com and spend some time on that website. The tab with my website is still open in a background. Then I switch from google tab back to my website tab. After doing that GSAP 'breaks' and scrolls two full-page sections one after another. Even if I scroll a mouse wheel a bit. And sometimes GSAP can't scroll the section at all. It looks like a full-page starts moving but suddenly the scrolling stops and GSAP places the current section to it's place.
I never go to any other website within that browser tab. Also I never go to any another route of my website. I just open an index page -> switch to another browser tab -> and switch back to my website tab. I have no idea how and why but exactly these actions cause the problem.
P.S. resizing the browser window helps but after some time the scrolling breaks again
Code:
export default function IndexPage() {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(ScrollToPlugin)
const sections = document.querySelectorAll("section")
const scrolling = {
enabled: true,
events: "scroll,wheel,touchmove,pointermove".split(","),
prevent: e => e.preventDefault(),
disable() {
if (scrolling.enabled) {
scrolling.enabled = false
window.addEventListener("scroll", gsap.ticker.tick, { passive: true })
scrolling.events.forEach((e, i) =>
(i ? document : window).addEventListener(e, scrolling.prevent, {
passive: false,
})
)
}
},
enable() {
if (!scrolling.enabled) {
scrolling.enabled = true
window.removeEventListener("scroll", gsap.ticker.tick)
scrolling.events.forEach((e, i) =>
(i ? document : window).removeEventListener(e, scrolling.prevent)
)
}
},
}
function goToSection(section, anim, i) {
console.log(scrolling.enabled)
if (scrolling.enabled) {
// skip if a scroll tween is in progress
scrolling.disable()
gsap.to(window, {
scrollTo: { y: section, autoKill: false },
onComplete: scrolling.enable,
duration: 1,
})
anim && anim.restart()
}
console.log(section)
}
sections.forEach((section, i) => {
const intoAnim = gsap.from(section.querySelector(".right-col"), {
yPercent: 50,
duration: 1,
paused: true,
})
ScrollTrigger.create({
trigger: section,
start: "top bottom-=1",
end: "bottom top+=1",
onEnter: () => goToSection(section, intoAnim),
onEnterBack: () => goToSection(section),
})
})
return () => {
ScrollTrigger.kill()
}
}, [])
return (
<div className={styles.container}>
<Header />
<Products />
<Contact />
<Footer />
</div>
)
}
Hi I use gsap and bootstrap
Each version is gsap 3.9.1 and bootstrap 5.1.3
My problem is that the end of the scroll trigger does not work properly.
const navbarani = gsap.from('.navbar', {
yPercent: -100,
paused: true,
duration: 0.5
}).progress(1)
ScrollTrigger.create({
start: 'top top',
end: 'bottom',
onUpdate: (self) => {
self.direction === -1 ? navbarani.play() : navbarani.reverse()
}
})
If I change end: 'bottom' to end: 999999 it works properly, but
I can't use end: 999999 because I want the scroll to work only when it's at the top.
What I don't understand is that if i erase the bootstrap from the cdn, end: 'bottom' works well.
That's why I think bootstrap and gsap scroll trigger's end cause conflict.
This is the code that I experimented with.
<template>
<div class="navbar">I'm navbar</div>
<div class="blank" />
</template>
<script>
import { onMounted } from 'vue'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
export default {
setup () {
onMounted(() => {
const navbarani = gsap.from('.navbar', {
yPercent: -100,
paused: true,
duration: 0.5
}).progress(1)
ScrollTrigger.create({
start: 'top top',
end: 'bottom',
onUpdate: (self) => {
self.direction === -1 ? navbarani.play() : navbarani.reverse()
}
})
})
}
}
</script>
<style>
.navbar {
position: fixed;
}
.blank {
height: 150vh;
}
</style>
I wonder if this collision only occurs to me or if the version is wrong.
If you don't have enough explanation, please let me know. I'll add an explanation.
Thank you for your help.