How to prevent keyframe animation to restart when scrolling again? - css

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 });

Related

Radial animated focus effect with mask-image in React TS

I am recreating this Radial animated focus effect with mask-image: Codepen I know I can just copy&paste the CSS into a .css file but I want to achieve the same result with a styled component. For that, I declared the CSS in my styled component and apply it. But I am not sure why nothing happens at all and what should I use instead of getElementById as manual DOM manipulation is bad practice?
App.tsx
import React from "react";
import styled from "styled-components";
const Property = styled.div`
#property --focal-size {
syntax: "<length-percentage>";
initial-value: 100%;
inherits: false;
}
`;
const FocusZoom = styled.div`
--mouse-x: center;
--mouse-y: center;
--backdrop-color: hsl(200 50% 0% / 50%); /* can't be opaque */
--backdrop-blur-strength: 10px;
position: fixed;
touch-action: none;
inset: 0;
background-color: var(--backdrop-color);
backdrop-filter: blur(var(--backdrop-blur-strength));
mask-image: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent var(--focal-size),
black 0%
);
transition: --focal-size .3s ease;
/* debug/grok the gradient mask image here */
/* background-image: radial-gradient(
circle,
transparent 100px,
black 0%
); */
}
`;
function App(bool: boolean) {
const zoom: Element = document.querySelector("focus-zoom");
const toggleSpotlight = (bool) =>
zoom.style.setProperty("--focal-size", bool ? "15vmax" : "100%");
window.addEventListener("pointermove", (e) => {
zoom.style.setProperty("--mouse-x", e.clientX + "px");
zoom.style.setProperty("--mouse-y", e.clientY + "px");
});
window.addEventListener("keydown", (e) => toggleSpotlight(e.altKey));
window.addEventListener("keyup", (e) => toggleSpotlight(e.altKey));
window.addEventListener("touchstart", (e) => toggleSpotlight(true));
window.addEventListener("touchend", (e) => toggleSpotlight(false));
return (
<>
<h1>
Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
</h1>
<FocusZoom></FocusZoom>
</>
);
}
export default App;
Check out solution with styled components
Code sandbox
import React, { useEffect } from "react";
import styled, { createGlobalStyle } from "styled-components";
export const GlobalStyle = createGlobalStyle`
body {
display: flex;
align-items: center;
justify-content: center;
}
/* custom properties */
:root {
--focal-size: {
syntax: "<length-percentage>";
initial-value: 100%;
inherits: false;
}
--mouse-x: center;
--mouse-y: center;
--backdrop-color: hsl(200 50% 0% / 50%);
--backdrop-blur-strength: 10px;
}
`;
const Wrapper = styled.div`
height: 400px;
width: 400px;
background: conic-gradient(
from -0.5turn at bottom right,
deeppink,
cyan,
rebeccapurple
);
`;
const FocusZoom = styled.div`
position: fixed;
touch-action: none;
inset: 0;
background-color: var(--backdrop-color);
backdrop-filter: blur(var(--backdrop-blur-strength));
mask-image: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent var(--focal-size),
black 0%
);
transition: --focal-size 0.3s ease;
`;
function App(bool) {
useEffect(() => {
const zoom = document.getElementById("zoomId");
const toggleSpotlight = (bool) =>
zoom.style.setProperty("--focal-size", bool ? "15vmax" : "100%");
window.addEventListener("pointermove", (e) => {
zoom.style.setProperty("--mouse-x", e.clientX + "px");
zoom.style.setProperty("--mouse-y", e.clientY + "px");
});
window.addEventListener("keydown", (e) => toggleSpotlight(e.altKey));
window.addEventListener("keyup", (e) => toggleSpotlight(e.altKey));
window.addEventListener("touchstart", (e) => toggleSpotlight(true));
window.addEventListener("touchend", (e) => toggleSpotlight(false));
toggleSpotlight();
}, []);
return (
<Wrapper>
<h1>
Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
</h1>
<FocusZoom id="zoomId"></FocusZoom>
</Wrapper>
);
}
export default App;
Also, ensure you have global styles & component imported in app file.
import Test, { GlobalStyle } from "./test";
export default function App() {
return (
<div className="App">
<GlobalStyle />
<Test />
</div>
);
}
As mentioned by others, we can simply refer to a DOM element in the React component template by using a useRef hook:
function App() {
// Get an imperative reference to a DOM element
const zoomRef = useRef<HTMLDivElement>(null);
const toggleSpotlight = (bool: boolean) =>
// To get the DOM element, use the .current property of the ref
zoomRef.current?.style.setProperty(
"--focal-size",
bool ? "15vmax" : "100%"
);
// Etc. including event listeners
return (
<>
<h1>
Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
</h1>
<FocusZoom ref={zoomRef} /> {/* Pass the reference to the special ref prop */}
</>
);
}
Demo: https://codesandbox.io/s/exciting-flower-349b48?file=/src/App.tsx
A more intensive solution could leverage styled-components props adaptation to replace the calls to zoom.style.setProperty(), as described in Jumping Text in React with styled component
In particular, this can help replace the use of CSS variables.
Except for --focal-size unfortunately, which is configured with a transition.
const FocusZoom = styled.div<{
focalSize: string; // Specify the extra styling props for adaptation
pointerPos: { x: string; y: string };
}>`
--focal-size: ${(props) => props.focalSize};
position: fixed;
touch-action: none;
inset: 0;
background-color: hsl(200 50% 0% / 50%);
backdrop-filter: blur(10px);
mask-image: radial-gradient(
circle at ${(props) => props.pointerPos.x + " " + props.pointerPos.y},
transparent var(--focal-size),
black 0%
);
transition: --focal-size 0.3s ease;
`;
function App() {
// Store all dynamic values into state
const [focalSize, setFocalSize] = useState("100%");
const [pointerPosition, setPointerPosition] = useState({
x: "center",
y: "center"
});
const toggleSpotlight = (bool: boolean) =>
// Change the state instead of messing directly with the DOM element
setFocalSize(bool ? "15vmax" : "100%");
// Etc. including event listeners
return (
<>
<h1>
Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
</h1>
{/* Pass the states to the styled component */}
<FocusZoom focalSize={focalSize} pointerPos={pointerPosition} />
</>
);
}
Demo: https://codesandbox.io/s/frosty-swirles-jdbcte?file=/src/App.tsx
This solution might be overkill for such case where the values change all the time (especially the mouse position), but it decouples the logic from the style implementation (the component does not know whether CSS variables are used or not).
Side note: for the event listeners, make sure to attach them only once (typically with a useEffect(cb, []) with an empty dependency array), and to remove them when the component is unmounted (typically by returning a clean up function from the useEffect callback).
You could also use useEvent from react-use for example, which hendles all that directly:
React sensor hook that subscribes a handler to events.
import { useEvent } from "react-use";
function App() {
// Attaches to window and takes care of removing on unmount
useEvent("pointermove", (e: PointerEvent) =>
setPointerPosition({ x: e.clientX + "px", y: e.clientY + "px" })
);
// Etc.
}
Instead of getElementById you should use the useRef hook

How to apply animation with changing text?

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

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

Transition not working with React useState hook and styled-component

When I click the button the transition happens instantly. How can I add a smooth in-and-out transition? I want the whole to slide nice and smooth when buttons are in focus.
const [margin, setMargin] = useState("-100vw");
const setStyle = (margin) => {
setMargin(margin);
};
const Box = styled.span`
display: block;
width: 150vw;
margin-top: 0;
height: 0;
margin-left: ${margin};
transition: all 0.8s 0.2s ease-in-out;
`;
return (
<Box>
<Wrapper>
{children}
<p>page contents</p>
<button onMouseEnter={() => setStyle("-100vw")}>Change</button>
</Wrapper>
<TriangleLeft>
<Closer>
<button onMouseEnter={() => setStyle("0")}>Change</button>
</Closer>
</TriangleLeft>
</Box>
);
};
I am not sure if this is an issue with css or how i am handling the hooks...
I take a look in your code and you just need to do some changes:
Remove all styled components out of Field component, like this:
const TriangleLeft = styled.span`
....
`;
const Box = styled.span`
....
`;
const Wrapper = styled.span`
....
`;
const Closer = styled.span`
....
`;
const Field = ({ children }) => {
const [margin, setMargin] = useState("-100vw");
const setStyle = (margin) => {
setMargin(margin);
};
return .....
}
Also, in your Box style, your margin-left should be:
/* Completed Style: */
const Box = styled.span<BoxProps>`
display: block;
width: 150vw;
margin-top: 0;
height: 0;
/* Here you are getting the prop margin and setting it to margin-left */
margin-left: ${props => props.margin};
transition: all 0.8s 0.2s ease-in-out;
`;
And finally in your Box tag:
return (
// Here you are saying that Box has a custom prop named margin and setting it with margin state.
<Box margin={margin}>
<Wrapper>
{children}
.....
You can check it here.

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.

Resources