Change css className of a div with animated transition - css

I am replacing the className of a div with button. But while doing this, I want to do it with animated transition while className change. In the code below, the className is changing, but without animated transition. How can I add the animated transition while class is changing?
import React, { useRef, useEffect, useState } from "react";
import classes from "./Card.module.css";
import { ClickAwayListener } from "#mui/base";
function Card(props) {
const [containerStyle, setStyle] = useState(classes.container);
const handleClick = () => {
setStyle(classes.containerMore);
};
const handleClickAway = () => {
setStyle(classes.container);
};
return (
<ClickAwayListener
mouseEvent="onMouseDown"
touchEvent="onTouchStart"
onClickAway={handleClickAway}
>
<div className={containerStyle}>
CONTAINER TITLE
<button className={classes.moreButton} onClick={handleClick}>MORE</button>
</div>
</ClickAwayListener>
);
}
export default Card;

This can be done just by writing some CSS transition in your code. You may do something like this :
.containerMore {
background-color: #05040f;
color: white;
height: 220px;
width: 220px;
border-style: solid;
text-align: center;
transition-property: background-color, width, height;
transition-duration: 1s;
transition-timing-function: linear;
}
now when the state is changing from the initial value classes.container to updated value classes.containerMore .it makes this change in the state a little smoother.
I made a demo in codesandbox that applies How You can add the animated transition while class is changing.

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 make dropdown animation?

I am implementing drop-down list using styled-component in react. In the process, I have two questions.
First, when dropDownVisible changes from true to false, why doesn't the animation effect apply and it disappears immediately? How can I improve the animation effect? Like when this list goes down, I want to make it gradually when it goes up.
Second, when StyledDropdown is dropped down, I want it to drop down behind the StyledHead, so I set the z-index property like that. I want the StyledHead to be always on top, so I'm curious why the StyledHead is hidden as the StyledDropdown drops down, even though I gave the z-index property bigger.
The source code is roughly structured like this:
// AApage.jsx
import { useEffect, useState, useRef } from 'react';
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
import styled, { keyframes } from 'styled-components';
const dropAnimation = keyframes`
0% {
transform : translateY(-300px);
display : none;
}
100% {
transform : translateY(0);
}
`;
const StyledHead = styled.div`
width: 100px;
height: 100px;
background-color: red;
z-index: 11;
`;
const StyledDropdown = styled.div`
width: 100px;
height: 300px;
background-color: #d9d9d9;
border-radius: 0px 0px 10px 10px;
z-index: 3;
animation: ${dropAnimation} 1s alternate;
`;
const AApage = () => {
const [dropDownVisible, setDropDownVisible] = useState<boolean>(false);
const toggleDropDownVisible = () => {
setDropDownVisible((prev) => !prev);
};
return (
<>
<StyledHead>
<div>Dropdown</div>
<span>{`${dropDownVisible}`}</span>
{dropDownVisible ? (
<MdArrowDropUp
onClick={() => {
toggleDropDownVisible();
}}
></MdArrowDropUp>
) : (
<MdArrowDropDown
onClick={() => {
toggleDropDownVisible();
}}
></MdArrowDropDown>
)}
</StyledHead>
{dropDownVisible ? (
<StyledDropdown>
<div>temp data</div>
<div>temp data</div>
<div>temp data</div>
</StyledDropdown>
) : (
<></>
)}
</>
);
};
export default AApage;

Change state in function component react

I am new to React and being held back by a seemingly simple task.
I've got a Header component nested within which is a HamburgerButton component. Clicking the latter should make a sidenav appear but for now I would like the icon to change from the 'hamburger' to the big 'X'.
Here is my parent component:
import { MyMoviesLogo } from 'components/Icons';
import HamburgerButton from 'components/HamburgerButton/HamburgerButton';
import styles from './Header.module.css';
const Header = (): JSX.Element => {
const [isActive, setIsActive] = useState(false);
return (
<header className={styles.header}>
<MyMoviesLogo className={styles.headerIcon} />
<HamburgerButton
isActive={false}
/>
</header>
);
};
export default Header;
And here is the HamburgerButton
import styles from './HamburgerButton.module.css';
type HamburgerButtonProps = {
isActive: boolean;
onClick?: () => void;
};
const addMultipleClassNames = (classNames: string[]): string => classNames.join(' ');
const HamburgerButton = ({ isActive, onClick }: HamburgerButtonProps): JSX.Element => {
return (
<div className={isActive ? addMultipleClassNames([styles.hamburger, styles.active]) : styles.hamburger} onClick={onClick}>
<div className={styles.bar}></div>
<div className={styles.bar}></div>
<div className={styles.bar}></div>
</div>
);
}
export default HamburgerButton;
Here's my HamburgerButton.module.css file:
.hamburger {
cursor: pointer;
display: block;
width: 25px;
}
.bar {
background-color: var(--hamburger-button-global);
display: block;
height: 3px;
margin: 5px auto;
transition: all 0.3s ease-in-out;
width: 25px;
}
.hamburger.active .bar:nth-child(2) {
opacity: 0;
}
.hamburger.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.hamburger.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
Manually changing the isActive prop to false verifies that the styling is applied as required.
My question is, how could I make it so when I click the icon its state gets toggled? I am familiar with React hooks like useState but can't quite put something together.
Any help would be greatly appreciated.
Thank you.
P.S.: It's probably obvious but I am using TypeScript.
You should use your onClick prop from your <HamburgerButton /> to change the parent state.
<HamburgerButton isActive={isActive} onClick={() => { setIsActive(oldState => !oldState) } />

React CSSTransition Issues

I am trying to display header one in the beginning and then when clicked on, fade in the second header. However they both are showing up in beginning. My understanding is that the in prop is what controls when the children of get rendered. But for some reason they are being rendered in beginning when showTwo is false. Any help would be much appreciated.
Component
import React, { useState } from "react";
import { CSSTransition } from "react-transition-group";
import "./Intro.css";
function Intro() {
const [showOne, setShowOne] = useState(true);
const [showTwo, setShowTwo] = useState(false);
return (
<>
{showOne && <h1 onClick={() => setShowTwo(true)}>One!</h1>}
<CSSTransition
in={showTwo}
timeout={750}
onEnter={() => setShowOne(false)}
classNames="creation"
>
<h2>Two!</h2>
</CSSTransition>
</>
);
}
export default Intro;
CSS
.creation-enter {
opacity: 0;
}
.creation-enter-active {
opacity: 1;
transition: opacity 700ms;
}
.creation-exit {
opacity: 1;
}
.creation-exit-active {
opacity: 0;
transition: opacity 700ms;
}
I realized that I was missing the prop unmountOnExit and misunderstanding what the in prop is for.
If others stumble upon this with similar issues, in is used to determine when the *-enter, *-enter-active, ... are added as classes.

Using ReactCSSTransitionGroup with styled-component

I'm using styled-components instead of tradition way of css. But I don't know how it can work together with ReactCSSTransitionGroup.
Basically, ReactCSSTransitionGroup looks for certain classnames in css resource, then apply to a component throughout its lifecycle. However, with styled-components, there are not any class names, styles are applied to components directly.
I know I can choose not to use ReactCSSTransitionGroup because the two technique doesn't look compatible. But when I use only styled-components, seems I can't render any animation when a component is unmounted - it's pure css, can't access component's lifecycle.
Any help or recommendation is appreciated.
I didn't want to use injectGlobal as suggested in another answer because I needed to make the transitions different per component.
It turns out to be pretty easy - just nest the transition classes in the styling for the component:
import React from "react";
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import styled from 'styled-components';
const appearDuration = 500;
const transitionName = `example`;
const Container = styled.section`
font-size: 1.5em;
padding: 0;
margin: 0;
&.${transitionName}-appear {
opacity: 0.01;
}
&.${transitionName}-appear-active {
opacity: 1;
transition: opacity ${appearDuration}ms ease-out;
}`;
export default () => {
return (
<CSSTransitionGroup
transitionName={transitionName}
transitionAppear={true}
transitionAppearTimeout={appearDuration}>
<Container>
This will have the appear transition applied!
</Container>
</CSSTransitionGroup>
);
};
Note that I'm using the newer CSSTransitionGroup, rather than ReactCSSTransitionGroup, but it should work for that too.
Mike Goatly's approach is great, but I had to make small changes to make it work. I changed the <CSSTransition>'s props, and used a function as its child.
See below for an example of a component, which fades in/out based on a state change:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { CSSTransition } from "react-transition-group";
import styled from "styled-components";
const Box = styled.div`
width: 300px;
height: 300px;
background: red;
transition: opacity 0.3s;
// enter from
&.fade-enter {
opacity: 0;
}
// enter to
&.fade-enter-active {
opacity: 1;
}
// exit from
&.fade-exit {
opacity: 1;
}
// exit to
&.fade-exit-active {
opacity: 0;
}
}`;
export default class App extends Component {
constructor() {
super();
this.state = {
active: true
};
setInterval(() => this.setState({ active: !this.state.active }), 1000);
}
render() {
return (
<CSSTransition
in={this.state.active}
classNames="fade"
timeout={300}
unmountOnExit
>
{() => <Box />}
</CSSTransition>
);
}
}
You can use css variable selector in styled-components. Like this:
const Animation = styled(ReactCSSTransitionGroup)`
${({ transitionName }) => `.${transitionName}-enter`} {
opacity: 0;
}
${({transitionName}) => `.${transitionName}-leave`} {
opacity: 1;
}
`
const animationID = 'some-hashed-text'
const AnimationComponent = props => (
<Animation
transitionName={animationID}
transitionEnterTimeout={0.1}
transitionLeaveTimeout={2000}
>
<div>some content</div>
</Animation>
)
Use the injectGlobal() styled-component helper method where your React app is bootstrapped. With this method you can style any CSS selector as if you'd be using conventional CSS.
First create a JS file exporting a template literal with your CSS for the react-transition-group (please not I'm using v2.1 new class names syntax):
globalCss.js
const globalCss = `
.transition-classes {
/* The double class name is to add more specifity */
/* so that this CSS has preference over the component one. */
/* Try removing it, you may not need it if properties don't collide */
/* https://www.styled-components.com/docs/advanced#issues-with-specificity */
&-enter&-enter {
}
&-enter&-enter-active {
}
&-exit&-exit {
}
&-exit&-exit-active {
}
}
`;
export default globalCss;
Then on your entry point file:
index.jsx
import { injectGlobal } from "styled-components";
import globalCss from "./globalCss.js";
injectGlobal`${ globalCss }`; // <-- This will do the trick
ReactDOM.render(
<Provider store={ Store } >
<HashRouter >
<Route path="/" component={ Component1 } />
<Route path="/" component={ Component2 } />
</HashRouter>
</Provider>,
document.getElementsByClassName("react-app")[0]
);
However, if you just use CSS/SASS/Less to write the classes for the react-trasition-group even when you use styled-components, it also works well.
There is a great blog post explaining how to do this:
https://dev.to/terrierscript/styled-component--react-transition-group--very-simple-transition-jja
They use a low level Transiton component available from react-transition-group:
http://reactcommunity.org/react-transition-group/transition
// This is overly simplified, but styles change depend on state from Transition
const MyStyledComponent = styled.div`
transform: translateY(${({ state }) => (state === 'exited' ? "0" : "-100%")});
transition: transform 2s;
`
const App = () =>
<Transition in={animate} timeout={500}>
{(state) => (
// state change: exited -> entering -> entered -> exiting -> exited
<MyStyledComponent state={state}>Hello</MyStyledComponent>
)}
</Transition>
import React from "react";
import { CSSTransition } from 'react-transition-group';
const styles = theme => ({
'fade-enter':{
opacity: 0,
},
'fade-enter-active':{
opacity: 1,
transition: "opacity 300ms"
},
'fade-exit':{
opacity: 1,
},
'fade-exit-active':{
opacity: 0,
transition: "opacity 300ms"
},
})
class myAnimatedComponent extends React.Component {
constructor(props){
super(props);
}
render(){
let {classes} = this.props;
return (
<CSSTransition
in={this.props.conditionVariable}
classNames={{
enter: classes['fade-enter'],
enterActive: classes['fade-enter-active'],
exit: classes['fade-exit'],
exitActive: classes['fade-exit-active'],
}}
timeout={300}
unmountOnExit>
<span>This will have the transition applied to it!</span>
</CSSTransition>
);
}
};
export default (styles)(myAnimatedComponent);
I had to use classes['fade-enter'] etc, because React changes the name of all classes in this component due to the fact that I used withStyles. And because of that too, when I export the component, React inserts my classes into this component's props, that's why I also had to create a variable called classes to catch those classes.

Resources