How to apply animation with changing text? - css

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

Related

ReactTransitionGroup animation not playing on state change

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).

How to prevent keyframe animation to restart when scrolling again?

I have a react page, whose main sections's visibility in the viewport are watched by an intersection observer. Once the section is visible by the user, a keyframe is triggered (usually a basic opacity transition from 0 to 1).
The thing is that the keyframes start again each time I scroll back up/down, which is annoying from a ux perspective. How to prevent keyframes animation to trigger again once they have happened?
Here is a code snippet:
import { useInView } from "react-intersection-observer";
const reveal = keyframes`
0% {opacity: 0;}
100% {opacity: 1;}
`;
const Section = styled.section`
margin: 64px 0;
`;
const SectionTitle = styled.h3<{ inView: boolean }>`
opacity: 0;
animation: ${({ inView }) =>
inView &&
css`
${reveal} 1s ease forwards
`};
`;
const Text = styled.p<{ inView: boolean }>`
opacity: 0;
animation: ${({ inView }) =>
inView &&
css`
${reveal} 1.5s ease forwards
`};
`;
export default function Home() {
const [about, aboutInView] = useInView();
const [pricing, pricingInView] = useInView();
return (
<div>
<Header />
<Section ref={about}>
<SectionTitle inView={aboutInView}>About</SectionTitle>
<Text inView={aboutInView}>about text</Text>
</Section>
<Section ref={pricing}>
<Text inView={pricingInView}>pricing text</Text>
</Section>
</div>
);
}
Set {triggerOnce: true} while calling the useInView Hook
// Generally
const [ref, inView, entry] = useInView({ threshold: 0, triggerOnce: true });
// Your Case
const [about, aboutInView] = useInView({ triggerOnce: true });

react-transition-group doesnt slideUp component React.js

I have a component where when o clicking the button, the div with more info will slideUp and slideDown.
Below the code and css style
import { CSSTransition } from "react-transition-group";
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore isOpen={showMoreInfo} click={() => setShowMoreInfo(!showMoreInfo)} />
<CSSTransition in={showMoreInfo} classNames="Card-Details" timeout={1000}>
<div>
{showMoreInfo && (
<>
<p>details</p>
<p>details</p>
</>
)}
</div>
</CSSTransition>
</div>
);
};
.Card-Details-enter {
height: 0px;
}
.Card-Details-enter-active {
height: 100%;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-enter-done {
height: 100%;
}
.Card-Details-exit {
height: 100%;
}
.Card-Details-exit-active {
height: 0px;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-exit-done {
height: 0px;
}
But it doesnt work, I don't know why. I tred to put the transition to the parent element like here
and add transition to the class *-exit-done like here, and nothing helped.
The reason why it doesn't work is because transitioning on percentage height isn't quite what you would expect.
Percentage height and width in CSS refer to their parent's height and width rather than their own height.
MDN Percentage
The CSS data type represents a percentage value. It is
often used to define a size as relative to an element's parent object.
Numerous properties can use percentages, such as width, height,
margin, padding, and font-size.
Example with the same element transitioning using a percentage height/width. One has a container element with a height/width of 50px, and the other doesn't.
div.container {
width: 50px;
height: 50px;
}
div.transition {
width: 100%;
height: 100%;
background: red;
transition: width 2s, height 4s;
}
div.transition:hover {
width: 300%;
height: 500%;
}
Transition div with a 50px container
<div class="container">
<div class="transition">
<p>test</p>
</div>
</div>
Transition div without a container:
<div class="transition">
<p>test</p>
</div>
What we'd actually want is to transition from 0px to auto height. Unfortunately browsers don't support transitioning on auto height.
A good write up of this is in Using CSS Transitions Auto Dimensions include some approaches to get what you want and their downsides.
Why hasn’t this problem been fixed at the browser level?
According to the Mozilla Developer Network docs, auto values have been
intentionally excluded from the CSS transitions spec. It looks like
it’s been requested by a few people, but when you think about it, it
makes at least a little sense that it hasn’t been included. The
browser process that re-calculates the sizes and positions of all
elements based on their content and the way they interact with each
other (known as “reflow”) is expensive. If you were to transition an
element into a height of auto, the browser would have to perform a
reflow for every stage of that animation, to determine how all the
other elements should move. This couldn’t be cached or calculated in a
simple way, since it doesn’t know the starting and/or ending values
until the moment the transition happens. This would significantly
complicate the math that has to be done under the hood and probably
degrade performance in a way that might not be obvious to the
developer.
How can I transition height: 0; to height: auto; using CSS? also has some good workarounds, though there really is no magic bullet for this.
It is definitely a very well known issue, and there's a request for the spec to change to allow transitions on auto, though I don't think it's gone anywhere yet.
As for support for the type of transition you are working on in React Transition Group:
Slide Down Animation and Trying to fade out element then slide up both have the same answer overall: pointing at how React Bootstrap's Collapse component does it.
You need to rely on finding the dom node's actual height and using that as part of the transition:
getDimension() {
return typeof this.props.dimension === 'function'
? this.props.dimension()
: this.props.dimension;
}
// for testing
_getScrollDimensionValue(elem, dimension) {
return `${elem[`scroll${capitalize(dimension)}`]}px`;
}
/* -- Expanding -- */
handleEnter = (elem) => {
elem.style[this.getDimension()] = '0';
}
handleEntering = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = this._getScrollDimensionValue(elem, dimension);
}
handleEntered = (elem) => {
elem.style[this.getDimension()] = null;
}
/* -- Collapsing -- */
handleExit = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = `${this.props.getDimensionValue(dimension, elem)}px`;
triggerBrowserReflow(elem);
}
handleExiting = (elem) => {
elem.style[this.getDimension()] = '0';
}
A quick and dirty example of using the functionality from the Collapse class for a working example of the code using a less fully featured solution (note, based heavily on the Collapse.js code linked above):
const { Transition } = ReactTransitionGroup;
const { EXITED, ENTERED, ENTERING, EXITING } = Transition;
const { useState } = React;
// Quick and dirty classNames functionality
const classNames = (...names) => names.filter((name) => name).join(' ');
const ButtonShowMore = ({ isOpen, click }) => {
return <button onClick={click}>{isOpen ? 'Close' : 'Open'}</button>;
};
// Heavily based on https://github.com/react-bootstrap/react-bootstrap/blob/next/src/Collapse.js#L150
// for the purpose of demonstration without just pulling in the module:
function triggerBrowserReflow(node) {
node.offsetHeight; // eslint-disable-line no-unused-expressions
}
const collapseStyles = {
[EXITED]: 'collapse',
[EXITING]: 'collapsing',
[ENTERING]: 'collapsing',
[ENTERED]: 'collapse in',
};
const Collapse = ({ children, ...props }) => {
const handleEnter = (elem) => (elem.style.height = '0');
const handleEntering = (elem) =>
(elem.style.height = `${elem.scrollHeight}px`);
const handleEntered = (elem) => (elem.style.height = null);
const handleExit = (elem) => {
elem.style.height = `${elem.scrollHeight}px`;
triggerBrowserReflow(elem);
};
const handleExiting = (elem) => (elem.style.height = '0');
return (
<Transition
{...props}
onEnter={handleEnter}
onEntering={handleEntering}
onEntered={handleEntered}
onExit={handleExit}
onExiting={handleExiting}
>
{(state, innerProps) =>
React.cloneElement(children, {
...innerProps,
className: classNames(
props.className,
children.props.className,
collapseStyles[state]
),
})
}
</Transition>
);
};
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore
isOpen={showMoreInfo}
click={() => setShowMoreInfo(!showMoreInfo)}
/>
<Collapse in={showMoreInfo} className="Card-Details" timeout={1000}>
<div style={{ height: 0 }}>
<p>details</p>
<p>details</p>
</div>
</Collapse>
</div>
);
};
ReactDOM.render(<Card />, document.querySelector('#root'));
.collapsing {
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
overflow: hidden;
}
.collapse {
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-transition-group/4.4.1/react-transition-group.min.js"></script>
<div id="root" />

React JS: How to animate conditionally rendered components?

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.

How to make slideIn AND slideOut with styled-components with react-animations

I'm working on building a component that displays none or block depending on it's isActive prop.
I've managed to make it slideInDown using react-animations with styled-components like this.
import styled, { keyframes } from 'styled-components';
import { ifProp } from 'styled-tools';
import { slideInDown } from 'react-animations';
const slideInAnimation = keyframes`${slideInDown}`;
const MyComponent = styled.div`
animation: 0.4s ${slideInAnimation};
display: ${ifProp('isActive', 'block', 'none')};
`;
I need to make it slideOutUp now. However, I'm not sure how to implement both slideInDown and slideOutUp animations together.
How would I accomplish that?
Note: that works. But I don't know how to make it both slide in and out. I tried something like this below but that didn't work.
import styled, { keyframes } from 'styled-components';
import { ifProp } from 'styled-tools';
import { slideInDown, slideOutUp } from 'react-animations';
const slideInAnimation = keyframes`${slideInDown}`;
const slideOutAnimation = keyframes`${slideOutUp}`;
const MyComponent = styled.div`
animation: 0.4s ${slideInAnimation}, 0.4s ${slideOutAnimation};
display: ${ifProp('isActive', 'block', 'none')};
`;
I went through the react-animations library, what I figured out is slideOutUp sets visibility: 'hidden.'
const slideOutUp: Animation = {
from: {
transform: translate3d(0, 0, 0)
},
to: {
visibility: 'hidden',
transform: translate3d(0, '-100%', 0)
}
};
You can use animation-fill-mode: forwards, which helps in retaining the style after the animation ends.
You can do something like this (it works):
const MyComponent = styled.div`
animation: ${ifProp(
"isActive",
`0.4s ${slideInAnimation}`,
`0.4s ${slideOutAnimation} forwards`
)};
`;
Here is the working example, for testing purpose onClick event I am setting {isActive: false}
https://codesandbox.io/s/949ql6p6no
Display does not animate. You need to use opacity for toggling (1 to 0 and vice versa) and animation.
const myComponent = styled.div`
animation: 0.4s ${slideInAnimation};
opacity: ${ifProp('isActive', 1, 0)};
`;
Considering that your code is working like you told me, you can use the attribute infinte at animation:
animation: 0.4s ${slideInAnimation} infinite;
I guess that it solves your problem.

Resources