I am trying to create an auto image slider with React and ReactTransitionGroup but I can't get animations to work. The image will change, but the transition effect won't play. I'm assuming it's the CSS because the class names do get added/removed.
Component
const ImgSlideshow = (props) => {
const images = [
'https://images.unsplash.com/photo-1593642634315-48f5414c3ad9?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80',
'https://images.unsplash.com/photo-1593642702821-c8da6771f0c6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1189&q=80',
'https://images.unsplash.com/photo-1593642532781-03e79bf5bec2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=634&q=80',
'https://images.unsplash.com/photo-1593642634443-44adaa06623a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=925&q=80',
'https://images.unsplash.com/photo-1593642634524-b40b5baae6bb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1189&q=80'
]
const [slide, setSlide] = useState(0);
useEffect(() => {
const slideshowTimer = setInterval(() => {
((slide + 1) > images.length - 1) ? setSlide(0) : setSlide(slide + 1)
}, 3000)
return () => clearInterval(slideshowTimer)
})
return (
<div className={styles.slideshow}>
<SwitchTransition>
<CSSTransition key={slide} timeout={200} classNames='slideshowImg'>
<img src={images[slide]} className="slideshowImg"></img>
</CSSTransition>
</SwitchTransition>
{props.children}
</div>
)
}
CSS
.slideshowImg-enter {
opacity: 0;
transform: translateX(-100%);
}
.slideshowImg-enter-active {
opacity: 1;
transform: translateX(0%);
}
.slideshowImg-exit {
opacity: 1;
transform: translateX(0%);
}
.slideshowImg-exit-active {
opacity: 0;
transform: translateX(100%);
}
.slideshowImg-enter-active,
.slideshowImg-exit-active {
transition: opacity 500ms, transform 500ms;
}
There are a couple of issues that I see.
You don't need to specify the className from the classNames prop on the child element that is being transitioned. If you have styles attached to this class then I would recommend you change it to something else to avoid confusion, but I don't think that is affecting your transitions.
You are setting transitions to 500ms but only allowing the transition group 200ms to complete the transition.
It also looks like you are just changing the image source instead of using a new element each time.
I have a working code sandbox here
I think you can achieve the desired result with this refactor:
const ImgSlideshow = (props) => {
const images = [
"https://images.unsplash.com/photo-1593642634315-48f5414c3ad9?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80",
"https://images.unsplash.com/photo-1593642702821-c8da6771f0c6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1189&q=80",
"https://images.unsplash.com/photo-1593642532781-03e79bf5bec2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=634&q=80",
"https://images.unsplash.com/photo-1593642634443-44adaa06623a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=925&q=80",
"https://images.unsplash.com/photo-1593642634524-b40b5baae6bb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1189&q=80"
];
const [slide, setSlide] = useState(0);
const imageElements = images.map((img, i) => {
return <img src={img} key={i} alt={`slideshow ${i+1}`} />;
});
useEffect(() => {
const slideshowTimer = setInterval(() => {
slide + 1 > images.length - 1 ? setSlide(0) : setSlide(slide + 1);
}, 3000);
return () => clearInterval(slideshowTimer);
});
return (
<div>
<SwitchTransition>
<CSSTransition key={slide} timeout={500} classNames="slideshowImg">
{imageElements[slide]}
</CSSTransition>
</SwitchTransition>
{props.children}
</div>
);
};
My snippet uses the map function to create an array of img elements, each with a src that is correspondent to the index of the images array. This way you will have multiple elements to transition in between. Right now you only have one image element, and the react-transition-group can't put an animation on the changing of the image src (afaik).
Related
I have added carousel functionality but the images are flickering whenever I click on next icon. It seems like I get the reflection of the previous image or the next one as I have only two images. I'm using tailwind with react. I'm adding a gif below so one can see the problem. I'm getting flickering every time. But my screen recorder shows it only the first time.
Link to Gif
Index.css
.slide-in-from-right {
animation: slideInFromRight 0.5s ease-in;
}
.slide-out-to-left {
animation: slideOutToLeft 0.5s ease-in;
}
#keyframes slideInFromRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
#keyframes slideOutToLeft {
from {
transform: translateX(0%);
}
to {
transform: translateX(-100%);
}
}
Hero SlideShow
let count = 0;
export default function HeroSlideshow() {
const [slide, setSlide] = useState({});
const [clonedSlide, setClonedSlide] = useState({});
const [slides, setSlides] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const slideRef = useRef();
const clonedSlideRef = useRef();
const { updateNotification } = useNotification();
const fetchLatestUploads = async () => {
const { error, movies } = await getLatestUploads();
if (error) return updateNotification("error", error);
setSlides([...movies]);
setSlide(movies[0]);
};
//0,1,2,3,4
const handleOnNextClick = () => {
setClonedSlide(slides[count]);
clonedSlideRef.current.classList.add("slide-out-to-left");
count = (count + 1) % slides.length;
setSlide(slides[count]);
setCurrentIndex(count);
slideRef.current.classList.add("slide-in-from-right");
};
const handleAnimationEnd = () => {
slideRef.current.classList.remove("slide-in-from-right");
clonedSlideRef.current.classList.remove("slide-out-to-left");
setClonedSlide({});
};
useEffect(() => {
fetchLatestUploads();
}, []);
return (
<div className="w-full flex">
{/* Slide show section */}
<div className="w-4/5 aspect-video relative overflow-hidden">
<img
ref={slideRef}
onAnimationEnd={handleAnimationEnd}
className="aspect-video object-cover"
src={slide.poster}
alt=""
/>
<img
ref={clonedSlideRef}
onAnimationEnd={handleAnimationEnd}
className="aspect-video object-cover absolute inset-0"
src={clonedSlide.poster}
alt=""
/>
<SlideShowController onNextClick={handleOnNextClick} />
</div>
{/* Up Next Section */}
<div className="w-1/5 aspect-video bg-red-300"></div>
</div>
);
}
const SlideShowController = ({ onNextClick, onPrevClick }) => {
const btnClass =
"bg-primary rounded border-2 text-white text-xl p-2 outline-none";
return (
<div className="absolute top-1/2 -translate-y-1/2 w-full flex items-center justify-between px-2">
<button onClick={onPrevClick} className={btnClass} type="button">
<AiOutlineDoubleLeft />
</button>
<button onClick={onNextClick} className={btnClass} type="button">
<AiOutlineDoubleRight />
</button>
</div>
);
};
It's difficult to answer if I cannot reproduce the issue but I have a clue of what's going on. When you call the method to handleOnNextClick you must take on mind that the slide's index (which is actually showing it as selected)'ll change almost instantly but the animation slideInFromRight and slideOutToLeft is taking .5 seconds to complete, so you can try to trigger the slide's index change with a setTimeout.
const handleOnNextClick = () => {
clonedSlideRef.current.classList.add("slide-out-to-left");
slideRef.current.classList.add("slide-in-from-right");
count = (count + 1) % slides.length;
setTimeout(() => {
setClonedSlide(slides[count]);
setSlide(slides[count]);
setCurrentIndex(count);
}, 500)
};
Hope it helps!
I'm trying to apply animation: smooth disappearing old text and smooth appearing the new one.
Now I created it with useEffect hooks and with inner Transition: onExited function
Furthermore I have not only title value, and my solution seemed to me the duct tape.
const [toggle, setToggle] = useState(true)
const [exited, setExited] = useState(false)
const [title, setTitle] = useState(service.title) //default value
useEffect(() => {
setExited(false) //set default value
setToggle(false)
}, [service]) //unmount node with old text (toggle this hook with changing another **service**)
useEffect(() => {
setToggle(true)
setTitle(service.title)
}, [exited]) //appear after old text unmounted
Node:
<Transition
in={toggle} timeout={500}
mountOnEnter unmountOnExit
onExited={() => setExited(true)}
>
{ state =>
<div className={classes['Services__card-categories-title'] + ' ' + classes[state]}>
{title}
</div>
}
</Transition>
Styles:
.entering{
animation: appearing .5s linear;
}
.exiting{
animation: appearing .5s linear reverse;
}
#keyframes appearing {
0%{
opacity: 0;
}
100%{
opacity: 1;
}
}
How to make it universal using react-transition-group library functionality ?
P.S. One more trouble is transition triggering not depends on single value, if any value is changed -> transition will triggered on every element
I'm trying to build a carousel but can't figure out the problem. So the first image loads fine, but every following image isn't shown. I'm guessing the component isn't rerendering, but I'm not 100%
const [curImage, setCurImage] = useState (0)
const images = ['/images/slideshow/slideshow1.webp', '/images/slideshow/slideshow2.webp','/images/slideshow/slideshow3.webp', '/images/slideshow/slideshow4.webp', '/images/slideshow/slideshow5.webp', '/images/slideshow/slideshow6.webp']
const goToNext = () => {
setCurImage(curImage === images.length - 1 ? 0 : curImage + 1);
}
useEffect(() => {
setTimeout(goToNext, 2000);
return function() {
clearTimeout(goToNext);
}
})
return (
<div className='Slideshow'>
{images.map((image, key) => (
<div
className={key === curImage ? 'Slideshow__Images Active':'Slideshow__Images'}
key={key}
aria-hidden={key !== curImage}
>
{key === curImage && (
<img src={`${image}`} alt='Gulfport Votes Images'/>
)}
</div>
))}
</div>
)
CSS
.Slideshow__Images {
opacity: 0;
transition: 0.3s ease;
}
.Slideshow .Active {
opacity: 1;
transition-duration: 0.2s;
}
Example is a functional component in which I am rendering a div conditionally. I want this div to fade-in when rendered conditionally and fade-out vice versa.
For that, I have maintained two local state variables: render and fadeIn which are computed based on show prop passed down to the Example component.
What I've done is:
When show prop it true, I set render as true, so the div renders conditionally and after a timeout of 10ms I set fadeIn as true which will set CSS classname for my div as show.
When show prop it false, I set fadeIn as false, which will set CSS classname for my div as hide and after a timeout of 200ms (transition time in CSS) I set render as false so the div is hidden conditionally.
Code:
interface Props {
show: boolean;
}
const Example: React.FC<Props> = ({ show, }) => {
const [render, setRender] = useState(false);
const [fadeIn, setFadeIn] = useState(false);
useEffect(() => {
if (show) {
// render component conditionally
setRender(show);
// change state to for conditional CSS classname which will
// animate opacity, I had to give a timeout of 10ms else the
// component shows up abruptly
setTimeout(() => {
setFadeIn(show);
}, 10);
} else {
// change state to change component classname for opacity animation
setFadeIn(false);
// hide component conditionally after 200 ms
// because that's the transition time in CSS
setTimeout(() => {
setRender(false);
}, 200);
}
}, [
show,
]);
return (
<div>
{render && (
<div className={`container ${fadeIn ? 'show' : 'hide'}`} />
)}
</div>
);
};
Stylesheet:
.container {
width: 100px;
height: 100px;
background-color: black;
transition: opacity 0.2s ease;
}
.show {
opacity: 1;
}
.hide {
opacity: 0;
}
I believe this is not a good coding practice to achieve the functionality and should maintain only one local state in my component. I need your suggestions on how I can solve this in a better way without using any 3rd Party Library.
Thanks :)
const [render, setRender] = useState(false);
useEffect(() => {
if(show) {
setTimeout(() => {
setRender(true);
}, 2000);
} else {
setRender(false);
}
}, [show]);
<div className={cs(s.render, render ? 'show' : undefined)}>
<p>{content}</p>
</div>
Css:
.render {
...,
visibility: hidden;
opacity: 0;
transition: all 0.6s ease;
}
.show {
visibility: visible;
opacity: 1;
}
Hope be helpful.
I am trying to build a reusable animated overlay.
However, only the first component mount is detected, meaning that I can only use this overlay once for my whole application. If I use a second one, it won't be animated.
I tried a lot of thing, like using unique keys, transition names and so on, but I can't get this to work.
Here is the current code:
import React from 'react';
import { CSSTransitionGroup } from 'react-transition-group';
import styled from 'styled-componets'
const OverlayDiv = styled.div`
transition: opacity 300ms ease-in;
&._overlay-transition-appear,
&._overlay-transition-enter {
opacity: 0.01;
}
&._overlay-transition-appear._loading-overlay-transition-appear-active,
&._overlay-transition-enter._loading-overlay-transition-enter-active {
opacity: 1;
}
&._overlay-transition-leave {
opacity: 1;
}
&._overlay-transition-leave._loading-overlay-transition-leave-active {
opacity: 0;
}
`;
const animated = (WrappedComponent) => ({ animate = true, ...otherProps }) => {
if (animate) {
return (
<CSSTransitionGroup
transitionName="_overlay-transition"
transitionAppear
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
transitionAppearTimeout={500}
>
<WrappedComponent {...otherProps} />
</CSSTransitionGroup>
);
}
return <WrappedComponent {...otherProps} />;
};
export const Overlay = ({ animate = true, className, parentDimensions, children }) => (
<OverlayDiv
key="overlay" // import for React CSS Transition
className={className}
height={parentDimensions ? `${parentDimensions.offsetHeight}px` : '100%'}
>
{children}
</OverlayDiv>
);
export default animated(Overlay);
Any idea on this ?