I have this basically:
<Motion defaultStyle={{opacity: 0}} style={{ opacity: spring(1, { stiffness: 210, damping: 20 }) }}>{style => {
return <div style={style}>
</Motion>
But the opacity chops and takes a while.
I just want to say "fade in ease-in-out 300ms". Can anything like this be done with react-motion? Or must you use react-transition-group?
I don't think that can be changed, but the velocity seems can be adjusted from stiffness and damping, https://github.com/chenglou/react-motion/issues/265
You can try a helper to figure out those values, http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser/
The trouble seems to me is the mount /dismount issue, but if you don't care, you could just setmount to be false.
const Fade = ({
Style, on, mount, children
}) => {
const [animating, setAnimating] = useState(true)
const onRest = () => { setAnimating(false) }
useEffect(() => { setAnimating(true) }, [on])
if (mount) {
if (!on && !animating) {
return null
}
}
return (
<Style
on={on}
onRest={onRest}
>
{children}
</Style>
)
}
Fade.propTypes = {
Style: elementType,
on: bool,
mount: bool,
}
You should not use the given "style" as the style prop
You should use it as such:
<Motion defaultStyle={{opacity: 0}} style={{ opacity: spring(1, { stiffness: 210, damping: 20 }) }}>{style => {
return <div style={{opacity: style.opacity}>
</Motion>
see my example here: fade example with delay using hooks
Related
I cannot find how to do a 3D carousel (aka slideshow) with React being able to show at least three elements.
There seems not to be up-to-date libraries or components for that in npm :(
Here is what it should look like:
After experimenting for a while, here is how I manage to do it using framer motion:
import './styles.css';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
export default function App() {
const [[activeIndex, direction], setActiveIndex] = useState([0, 0]);
const items = ['🍔', '🍕', '🌭', '🍗'];
// we want the scope to be always to be in the scope of the array so that the carousel is endless
const indexInArrayScope =
((activeIndex % items.length) + items.length) % items.length;
// so that the carousel is endless, we need to repeat the items twice
// then, we slice the the array so that we only have 3 items visible at the same time
const visibleItems = [...items, ...items].slice(
indexInArrayScope,
indexInArrayScope + 3
);
const handleClick = newDirection => {
setActiveIndex(prevIndex => [prevIndex[0] + newDirection, newDirection]);
};
return (
<div className="main-wrapper">
<div className="wrapper">
{/*AnimatePresence is necessary to show the items after they are deleted because only max. 3 are shown*/}
<AnimatePresence mode="popLayout" initial={false}>
{visibleItems.map((item) => {
// The layout prop makes the elements change its position as soon as a new one is added
// The key tells framer-motion that the elements changed its position
return (
<motion.div
className="card"
key={item}
layout
custom={{
direction,
position: () => {
if (item === visibleItems[0]) {
return 'left';
} else if (item === visibleItems[1]) {
return 'center';
} else {
return 'right';
}
},
}}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 1 }}
>
{item}
</motion.div>
);
})}
</AnimatePresence>
</div>
<div className="buttons">
<motion.button
whileTap={{ scale: 0.8 }}
onClick={() => handleClick(-1)}
>
◀︎
</motion.button>
<motion.button whileTap={{ scale: 0.8 }} onClick={() => handleClick(1)}>
▶︎
</motion.button>
</div>
</div>
);
}
const variants = {
enter: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? 50 : -50, opacity: 0 };
},
center: ({ position }) => {
return {
scale: position() === 'center' ? 1 : 0.7,
x: 0,
zIndex: zIndex[position()],
opacity: 1,
};
},
exit: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? -50 : 50, opacity: 0 };
},
};
const zIndex = {
left: 1,
center: 2,
right: 1,
};
Here is a code sandbox with the solution:
https://codesandbox.io/s/react-3d-carousel-wth-framer-motion-rtn6vx?file=/src/App.js
I have a component that currently uses the useDrag hook to connect to react-dnd. It works well, except for previews. I want to implement useDragLayer instead to see if it would help with my preview problems, as many online threads suggest.
This is my current (simplified) useDrag implementation:
const [{ isDragging }, connectDragSource, connectPreview] = useDrag({
item,
collect: monitor => ({
isDragging: monitor.getItem()?.index === item.index,
})
})
return (
<Wrapper ref={connectPreview} isDragging={isDragging}>
<DragHandle ref={connectDragSource} />
</Wrapper>
)
How do I use useDragLayer in this context, in a way that might help with my previews? The docs example makes little sense to me...
How do I connect my rendered components using useDragLayer api? useDragLayer doesn't return drag source and preview connector functions (like useDrag does on index 1 and 2 of the returned array), and its collect function doesn't provide a DragSourceConnector instance either. So what do I do with the hook/returned value after I call it?
I just resolved this and want to share it to help others :)
You will need to do couple of things for this to fully work.
Disable the default preview behavior by adding the following useEffect
import { getEmptyImage } from "react-dnd-html5-backend";
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: "BOX",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
Create the custom default layer
export const CustomDragLayer = (props: {}) => {
const {
itemType,
isDragging,
initialCursorOffset,
initialFileOffset,
currentFileOffset,
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialCursorOffset: monitor.getInitialClientOffset(),
initialFileOffset: monitor.getInitialSourceClientOffset(),
currentFileOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(
initialCursorOffset,
initialFileOffset,
currentFileOffset
)}
>
<div>Your custom drag preview component logic here</div>
</div>
</div>
);
};
const layerStyles: CSSProperties = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
border: "10px solid red",
};
function getItemStyles(
initialCursorOffset: XYCoord | null,
initialOffset: XYCoord | null,
currentOffset: XYCoord | null
) {
if (!initialOffset || !currentOffset || !initialCursorOffset) {
return {
display: "none",
};
}
const x = initialCursorOffset?.x + (currentOffset.x - initialOffset.x);
const y = initialCursorOffset?.y + (currentOffset.y - initialOffset.y);
const transform = `translate(${x}px, ${y}px)`;
return {
transform,
WebkitTransform: transform,
background: "red",
width: "200px",
};
}
Add the <CustomDragLayer /> to the top-level component
You will need to include the ref={drag} to the component you want to drag and remove the connectPreview ref completely.
Hopefully, this helps you.
According to the docs I can make variant properties dynamic: https://www.framer.com/docs/animation/##dynamic-variants.
But this doesn't work when I try to make the initial properties dynamic.
For example:
import React, { useState, useEffect } from "react";
import { motion, useAnimation } from "framer-motion";
//make div appear from either bottom or right, depending on "origin" custom prop
const variant = {
hidden: (origin) =>
origin === "bottom"
? { x: 0, y: 200, opacity: 0 }
: { x: 200, y: 0, opacity: 0 },
visible: { x: 0, y: 0, opacity: 1, transition: { duration: 1 } },
};
function App() {
const [origin, setOrigin] = useState("bottom");
const controls = useAnimation();
//after 2 secs make origin "right"
useEffect(() => {
setTimeout(() => {
setOrigin("right");
}, 2000);
}, []);
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
controls.start("visible");
}, 4000);
}, [controls]);
return (
<motion.div
style={{ width: 100, height: 50, background: "red" }}
variants={variant}
initial="hidden"
animate={controls}
custom={origin}
/>
);
}
export default App;
Here I made a dynamic variant to make a div appear from either the right or bottom, which I can control from a custom prop. Initially this custom prop is set to "bottom". After 2 secs, this is changed to "right". When I start the animation after 4 secs, I expect the div to appear from the right but it still appears from the bottom:
This is because the component is already rendered and is still the same component even if the origin prop being passed to the component has changed.
You can do two things:
Use a isVisible state variable where the render method will observe for changes and render the component when it becomes true.
function App() {
const [isVisible, setIsVisible] = useState(false);
...
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
controls.start("visible");
}, 4000);
}, [controls]);
return (
isVisible && (
<motion.div
...
/>
)
);
}
DEMO
Add a key prop to the component with the origin value so that when the value changes, React will re-render the component.
function App() {
...
return (
<motion.div
key={origin}
...
/>
);
}
DEMO
2nd option may be your preferred choice if you need to toggle between the origin.
I'm trying to change opacity of the span below so that I can show the user that the text is copied.
before copyToClipboard clicked => opacity: 0
after clicked => opacity: 1 for about 1 sec and again to opacity:0
I know onCopy={timer} wouldn't work but I really can't figure out how to approach.
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
useEffect(() => {
const timer = setTimeout(function () {
setStyle({ opacity: 1 })
}, 1000)
}, [])
return (
<div>
<CopyToClipboard text='something' onCopy={timer}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
I think you don't need useEffect for this case. Just create timer function outside useEffect like below:-
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
const timer = () => {
setStyle({ opacity: 1 });
setTimeout(() => {
setStyle({ opacity: 0 });
}, 1000);
};
return (
<div>
<CopyToClipboard text='something' onCopy={() => timer()}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
)
Instead of using setTimeout,you can use simple CSS animations to achieve the following -
Create a CSS animation using keyframes
Create a state to cause the animation and change state onCopy.
And that's it.
Check the code here, for ref-
https://codesandbox.io/s/festive-northcutt-kb32u?file=/src/App.js
I personally would not use timers for UI updates - ever. It does work (in this case), but it is not clean. I would suggest using CSS and a transition or animation. Removing/clearing the class name in onAnimationEnd makes sure the animation will trigger every time it is clicked. The animation duration is set to 1,4s out of which (0,2/1,4) = 14,29% are for fading in and 14,29% for fading out, that leaves 1s for the span to be shown
CSS:
.copied {
opacity: 0;
display:none;
}
.flash {
display:inline-block;
animation: flash 1.4s ease-in-out both;
}
#keyframes flash {
0: {
opacity: 0;
}
14.29% {
opacity: 1;
}
85.71% {
opacity: 1;
}
100% {
opacity: 0
}
}
(Simplified your example to remove the dependency on clipboard):
const Contact = () => {
const [flash, setFlash] = React.useState("")
const onClick = (event) => {
setFlash("flash");
}
const onAnimationEnd = (event) => {
setFlash("");
}
return (
<div onClick={onClick}>
Something
<span className={`copied ${flash}`} onAnimationEnd={onAnimationEnd}>
copied!
</span>
</div>
)
}
Like in your example this has the downside that the element has to exist all the time (even though now with display:none removed from rendering).
An even better approach would be to use the amazing TransitionGroup/CSSTransition from react-transition-group to add/remove the element. Admittedly, a bit much for this example, but in general the better and cleaner way to go.
I am trying to figure out how to utilise useTransition for page transitions (simple opacity change where first page fades out and new one fades in).
So far I have this small demo going https://codesandbox.io/s/sleepy-knuth-xe8e0?file=/src/App.js
it somewhat works, but weirdly. When transition starts new page is mounted instantly while old one starts animating. This causes various layout issues and is not behaviour I am after. Is it possible to have first element fade out and only then mount and fade in second element?
Code associated to demo
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}
you can delay the start of the transition by waiting for the leave animation to complete.
const sleep = t => new Promise(res => setTimeout(res, t));
...
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: i => async next => {
await sleep(1000);
await next({ opacity: 1 });
},
leave: { opacity: 0 }
});
This delays the animation also for the very first time it is run. You can have a ref to keep track of whether the component has been rendered before or if it is its first time rendering, then you can skip sleep call if it's the first render.
OR
You can just simply provide trail config
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
trail: 300
});
You need to add position: absolute and then you need to set the right position with css.
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}