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" />
Related
I'm trying to translate an element such that it always stays on the screen, but it seems that it's leaving some whitespace on the bottom. I can't use position absolute as I want high performance on the animation. I have tried making the width and height 0 and changing the transform-origin property, but nothing has worked so far. Here is my website and the relevant code.
JSX:
useEffect(() => {
const onScroll = () => {
let test = document.getElementsByClassName('scrollToTop')[0];
test.style.transform =
'translate(calc(100vw - 36px - 10px),' +
(window.scrollY -
document.body.scrollHeight +
window.innerHeight -
10) +
'px)';
console.log(test.style.transform);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
CSS:
.scrollToTop {
/* opacity: 0; */
transform: translate(0, 0);
visibility: none;
background: var(--blue);
transition: opacity 0.5s ease-in-out;
display: inline-block;
}
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
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 have a header, whose className changes depending on State. Each class has a different background image, specified in the CSS. Everything works fine, but the transitions are quite abrupt without a fade-in effect.
I wrote:
.jumbotron-img-1{
background-image: url("/images/myImg1.jpg");
transition: all 1s ease-in-out;
It works, but it's ugly. There is a zoom, and a distortion of the image before it shows up in its final form. I've watched some tutorials on Google, but nothing was simple and to the point for background-image transition in pure CSS or React.
Any help would be greatly appreciated, thanks!
background-image is not an animatable property. I feel what best serves your purpose is to render multiple headers with all the classnames available stacked over each other with position: absolute; relative to common parent and make only one of them visible using opacity property based on which classname is active in your state and use transition on opacity
Sample working code:
render() {
const {imgClassList} = this.props;
const {activeimgClass} = this.state;
return (
<div className="header-container">
{imgClassList.map(imgClass => {
return (
<div
className={`header ${imgClass} ${(imgClass === activeimgClass)? 'active' : ''}`}
/>)
})}
</div>
)
}
And css be something like:
.header-container {
position: relative;
}
.header{
position: absolute;
top: 0;
left: 0;
opacity: 0
transition: opacity 1s ease-in-out;
}
.header.active {
opacity: 1
}
.img-1 {
background:url('images/img-1')
}
.img-2 {
background: url('images/img-2')
} ... and so on
There's no good way to transition a background image using CSS because it's not an animatable property, per the CSS spec. One way to do this is to just have multiple images on top of one another, each containing a different one of the images you'd like to display, and then cycle through them by transitioning them to opacity: 0 and changing their z-index order.
I made a quick demo showing how you can achieve smooth changes by manipulating opacity and z-index. In pure Javascript, this is done by simply adjusting the styles with DOM manipulation and using setTimeout().
Of course in React you don't want to be doing DOM manipulation, so you can experiment with multiple classes with different opacity levels and transitions to accomplish this. There also seems to be a React component that enables all types of transitions: https://reactcommunity.org/react-transition-group/css-transition
Check out the Javascript solution demo to see how changing the opacity can get a crossfade effect on images:
function backgroundScheduler_1() {
setTimeout(() => {
document.querySelector(".img1").style.opacity = 0;
document.querySelector(".img2").style.opacity = 1;
document.querySelector(".img3").style.opacity = 1;
order(["-3", "-1", "-2"], () => { backgroundScheduler_2() }, 1000);
}, 3000);
}
function backgroundScheduler_2() {
setTimeout(() => {
document.querySelector(".img1").style.opacity = 1;
document.querySelector(".img2").style.opacity = 0;
document.querySelector(".img3").style.opacity = 1;
order(["-2", "-3", "-1"], () => { backgroundScheduler_3() }, 1000);
}, 3000);
}
function backgroundScheduler_3() {
setTimeout(() => {
document.querySelector(".img1").style.opacity = 1;
document.querySelector(".img2").style.opacity = 1;
document.querySelector(".img3").style.opacity = 0;
order(["-1", "-2", "-3"], () => { backgroundScheduler_1() }, 1000);
}, 3000);
}
function order(array, callback, time) {
setTimeout(() => {
document.querySelector(".img1").style.zIndex = array[0];
document.querySelector(".img2").style.zIndex = array[1];
document.querySelector(".img3").style.zIndex = array[2];
callback();
}, time);
}
backgroundScheduler_1();
.background-image {
position: absolute;
top: 0;
left: 0;
opacity: 1;
transition: 1s;
}
.img1 {
z-index: -1;
}
.img2 {
z-index: -2;
}
.img3 {
z-index: -3;
}
<div class="background-container">
<img class="background-image img1" src="https://placeimg.com/640/640/nature"></img>
<img class="background-image img2" src="https://placeimg.com/640/640/animals"></img>
<img class="background-image img3" src="https://placeimg.com/640/640/tech"></img>
<h2 style="color: white;">WOW!</h2>
</div>
I checked NPM momentarily and didn't see anything that promises this exact functionality. Hope this helps!
I've written a React app, using CSS transitions. But those transitions does not work correctly in some of the components. In my app, only the components who are moving upwards works well, those who are moving downwards moves instantly without animation. (I want them both moves with animation.)
Here is the CSS I used there:
div.canvas {
position: absolute;
top: 90px;
left: 60px;
width: 640px;
height: 480px;
border: 1px solid #999;
background: white;
}
div.canvas-rect {
position: relative;
margin-left: 20px;
margin-top: 10px;
height: 20px;
background: green;
transition: all 1s linear;
-moz-transition: all 1s linear; /* Firefox 4 */
-webkit-transition: all 1s linear; /* Safari 和 Chrome */
-o-transition: all 1s linear; /* Opera */
}
UPDATED:
I also built a codepen.io project to show the problem. It has the complete code of this demo project.
I've tried to add a log entry to componentDidUpdate, componentDidMount and componentWillUnmount methods to show whether these component are re-created or updated, it shows that they are all updated (not re-created, or removed) every second.
Well, after I started a bounty because I also have this problem I finally found what seems to be the problem.
When you are using absolute position (or relative, as in your case), if you re-render the whole list every time, React will re-order the elements in the DOM (as you said, the elements are not being recreated, just updated). But this creates the problem with the transitions... apparently, if you move an element while the transition is running then you end up cutting the animation.
So, for cases in which you want to use position absolute, the key concept is to render the containers of your elements once (in this case, just divs) and only change the inner contents based on the new order. If you need to add more elements, just add them at the end.
I modified your codepen so that it reflects what I am saying. My example is very dumb because I just created 4 ad-hoc divs, but it illustrates the idea: create as many containers as you need, but DO NOT use a map that recreates them every time, or your transitions will be cut.
https://codepen.io/damianmr/pen/boEmmy?editors=0110
const ArrList = ({
arr
}) => {
return (
<div style={{position: 'relative'}}>
<div className={`element element-${arr[0]} index-${arr[0]}`}>{arr[0]}</div>
<div className={`element element-${arr[1]} index-${arr[1]}`}>{arr[1]}</div>
<div className={`element element-${arr[2]} index-${arr[2]}`}>{arr[2]}</div>
<div className={`element element-${arr[3]} index-${arr[3]}`}>{arr[3]}</div>
</div>
);
}
So, the problem is basically how you create a static list of containers and how you iterate through that list so that the first container renders the first element of your data, the second container the second element, etc.
Hope that it helps, this problem was driving me crazy too! :)
I know this wasn't the case, but since I got here also looking for React css transition does not work correctly, I just wanted to share:
If you create an element using arrow functions inside render, it won't get properly animated, since a new componente is always being created.
You should create a function outside and invoke it in 'render'.
You can trick React by using index as key. If you think about el, and index as starting position (index) and end position (el), the element has moved to the old end position by the end of the transition, and by when it's there, it's taken over by the new start position and (index) is switched to match the new setup. This is because when you set key in an element in react, the virtual DOM will always interpret it as it is the same element. And for the sake of it, you're right in setting index as the "id" in general.
I made a working example only by switching index/el (and setting element position to absolute).
const {combineReducers, createStore} = Redux;
const { Provider, connect } = ReactRedux;
const ArrList = ({
arr
}) => (
<div>{
arr.map((el, index)=>
<div
key={""+index}
className={`element element-${el}` + ` index-${el}`}
>
{el}
</div>) }
</div>
)
const mapStateToArrList = (state) => {
return {
arr: state.appReducer.arr
}
};
const App = connect(mapStateToArrList, null)(ArrList);
const initialState = {
arr: [1, 2, 3, 4]
}
const appReducer = (state = initialState, action) => {
switch(action.type) {
case "tick":
return {
...state,
arr: _.shuffle(state.arr)
}
default:
return state
}
}
const reducer = combineReducers({
appReducer
})
const store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
const dispatcher = () => {
store.dispatch({
type: "tick"
})
setTimeout(dispatcher, 1000)
}
dispatcher()
.element {
position: absolute;
height: 20px;
background: green;
margin-left: 20px;
margin-top: 20px;
text-align: right;
color: white;
line-height: 20px;
transition: all 1s ease-in;
-moz-transition: all 1s ease-in; /* Firefox 4 */
-webkit-transition: all 1s ease-in; /* Safari 和 Chrome */
-o-transition: all 1s ease-in; /* Opera */
}
.element-1 {
width: 20px;
}
.element-2 {
width: 40px;
}
.element-3 {
width: 60px;
}
.element-4 {
width: 80px;
}
.index-1 {
top: 20px;
}
.index-2 {
top: 40px;
}
.index-3 {
top: 60px;
}
.index-4 {
top: 80px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<div id="root"></div>
If you are removing the element from the virtual DOM, then the react will update its contents, so you won't see the animations. What you can do is either use react-transition-group OR tell your app to wait x ms before updating the dom once the event is called OR use visibility to toggle between hidden and showing instead of removing it completely from the DOM.
You did recreate DOM elements each time.
You should define collect key value.
I changed your key value '' + el to '' + index.
<div key={'' + index} className={'element element-' + el + ' index-' + index} >
Just change css properties only :)
I''ll just leave that here in case helps somebody but to me it was solved by moving from
export default function Contact(props) {...}
To:
const Contact = (props) => {...}
export default Contact