I'm building a simple App in react. And I'm trying to change an svg fill color when I click on like button. I've researched around and many developers use React Transition Group as a way, but I'm having hard time understanding how it works in this case.
Each time I click on like button, it increments the number and I want to change the color of svg background when it does. I've tried this way but it doesn't work. What am I doing wrong here?
Here is code.
<button onClick={this.props.increment.bind(null,index)}>
<span>{this.state.portfLikes}</span>
<CSSTransition
key="heart"
classNames="anim"
timeout={{ enter: 500, exit: 300 }}>
<span className="like-icon" key="heart-icon"><Like/></span>
</CSSTransition>
</button>
Like svg as a component.
import React from 'react'
class Like extends React.Component{
render() {
return (
<svg viewBox="0 0 64 64" width="64px" xmlns="http://www.w3.org/2000/svg">
<g id="Layer_1"><g><circle cx="32" cy="32" fill="#a3bed7" r="32"/></g><g opacity="0.2"><g><path d="M49.982,31.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099 c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,31.464,50,31.34,50,31.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z" fill="#231F20"/></g></g><g><g><path d="M49.982,29.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,29.464,50,29.34,50,29.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z" fill="#f5f5f5"/></g></g></g><g id="Layer_2"/></svg>
)
}
}
export default Like
CSS
Note: I've picked opacity as a test to see if the animation works.
.anim-enter {
opacity: 0.12;
}
.anim-enter.anim-enter-active svg {
opacity: 0.5;
transition: opacity 500ms ease-in;
}
.anim-exit {
opacity: 1;
}
.anim-exit.anim-exit-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
I'm also open to suggestions about other performance friendly react animation tools.
I see Sebastian has already provided an answer for using css styles and the hover pseudo element. It did seem like you wanted it to happen on a click though.
CSSTransition is only really needed when you want to animate elements as they are added to the DOM. If the items already exist, you just use all the typical css jazz to animate items (transitions and transforms).
Here is an example using react lifecycle functions to add an event listener and animate the heart when a button is clicked.
See the codepen here https://codepen.io/bluesixty/pen/gGzbrj?editors=0111.
Note: there is something goofy with codepen in the callback on the eventlistener. Codepen is not calling animatingDone to change the state back to false. (I tested on my local and it works correctly). Just refresh the window to reset it.
Javascript
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
animate: false
};
}
//------------------------------------------------------------------------
// Handle the click animations
//------------------------------------------------------------------------
componentDidMount() {
const svgToPulse = this.itemToPulse;
svgToPulse.addEventListener("animationend", this.animatingDone);
}
componentWillUnmount() {
const svgToPulse = this.itemToPulse;
svgToPulse.removeEventListener("animationend", this.animatingDone);
}
animatingDone() {
this.setState({ animate: false });
}
pulse() {
this.setState({ animate: true });
}
render() {
return (
<div>
<button onClick={() => this.pulse()}>Clicky</button>
<svg
ref={e => {
this.itemToPulse = e;
}}
className={this.state.animate ? " animate-pulse" : ""}
viewBox="0 0 64 64"
width="64px"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Layer_1">
<g>
<circle cx="32" cy="32" fill="#a3bed7" r="32" />
</g>
<g opacity="0.2">
<g>
<path
d="M49.982,31.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099 c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,31.464,50,31.34,50,31.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z"
fill="#231F20"
/>
</g>
</g>
<g>
<g>
<path
d="M49.982,29.003c-0.094-5.522-4.574-10.442-10.107-10.442c-3.2,0-6.019,1.674-7.875,4.131 c-1.856-2.457-4.676-4.131-7.875-4.131c-5.533,0-10.012,4.921-10.107,10.442H14c0,0.034,0.007,0.065,0.007,0.099c0,0.025-0.007,0.049-0.007,0.076c0,0.155,0.038,0.272,0.045,0.421c0.495,14.071,17.813,19.84,17.813,19.84 s17.572-5.762,18.092-19.818C49.959,29.464,50,29.34,50,29.178c0-0.027-0.007-0.052-0.007-0.076c0-0.036,0.007-0.065,0.007-0.099 H49.982z"
fill="#f5f5f5"
/>
</g>
</g>
</g>
<g id="Layer_2" />
</svg>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
And the css
.animate-pulse {
animation: animate-pulse 300ms;
}
#keyframes animate-pulse {
0% {transform: scale(1.2)}
50% {transform: scale(1.5)}
100% {transform: scale(1.2)}
}
How it works
Clicking the button sets the animate state to true
React responds to the state change and the animate-pulse class is added to the svg element
the eventlistener waits for the animation to end and makes the call to set the animate state back to false.
react responds to the state change and removes the animate-pulse class
You might try vanilla CSS - see this simplified example with a button:
<button>
<svg height="20" width="20"><circle cx=10 cy=10 r=8 /></svg>
</button>
per se, it's just a button with a circle. I format this using CSS:
button circle {
fill: yellow;
}
as soon as I extend the CSS to do an animation on focus, I think I already achieve what you are looking for
button circle {
fill: yellow;
transition: fill 2s;
}
button:focus circle {
fill: green;
}
The button changes from yellow to green when clicked. Instead of focus, you could also assign CSS classes.
I've put the exact same code into a codepen if you want to try directly: https://codepen.io/sebredhh/pen/NaMPoP
Related
i want to make sequential images that fade in and out repeated.
i seen this sometimes in web.
i want to implement this in app with react-native.
but i feel difficult when make this with pure react native.
and it seems impossible with ImageBackground Component even if i use image show background of component.
when i use multiple ImageBackground, my react app cannot show image correctly.
my image seems to be hidden.
and i feel difficult when i design animation with complex delay.
how to make this?
thanks for ans!
react native animated : why my loop image animation is worked like a weird?
i have successfully created App behavior i want.
so i want to share this
we have to know this
absolute position
useref
animated
first, save and declare images
const MainScreen = () => {
const imageSources = [
require('../assets/image/meadow.jpg'),
require('../assets/image/mountain.jpg'),
require('../assets/image/sea.jpg'),
require('../assets/image/desert.jpg'),
]
return (
<View style={{ styles.yourLayout, }}>
<YourComponents />
</View>
);
};
export default MainScreen;
second, make custom image component
import React, {useRef,useEffect} from "react";
import { Animated, } from 'react-native';
interface BackgroundImageProps{
delay: number,
imageSource: any,
}
const CustomImage= ({delay, imageSource}:BackgroundImageProps) => {
const fadeAni = useRef<Animated.Value>(new Animated.Value(0)).current;
//opacity 1: visible, 0: invisible
useEffect(() => {
//make fadein <-> fadeout loop
}, [fadeAni])
return (
<Animated.Image style={{
opacity: fadeAni,
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
}} source={imageSource} />
);
}
/*
*/
export default BackgroundImage;
third, make animation
we want to start animation when as soon as component rendered,
we use useEfecct
since other image to be invisible when other image visible,
we design the animation sequence(delay,loop). so we can start each loop for each images.
when animated value is increased to 1,
show image 2secs
when animated value is decreased to 0,
wait for other image.
Animated.sequence([
Animated.delay(delay),
Animated.loop(
Animated.sequence([
Animated.timing(fadeAni, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.delay(2000),
Animated.timing(fadeAni, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
Animated.delay(8000),
]),
)
]).start()
we must add animated value dependency in useEffect. (like the code)
if you want to know more click the url in my question.
apply
const MainScreen = () => {
const imageSources = [
require('../assets/image/meadow.jpg'),
require('../assets/image/mountain.jpg'),
require('../assets/image/sea.jpg'),
require('../assets/image/desert.jpg'),
]
return (
<View style={{ styles.yourLayout, }}>
<BackgroundImage imageSource={imageSources[0]} delay={0} />
<BackgroundImage imageSource={imageSources[1]} delay={3000} />
<BackgroundImage imageSource={imageSources[2]} delay={6000} />
<BackgroundImage imageSource={imageSources[3]} delay={9000} />
<YourComponents />
</View>
);
};
backgroundimage component's position is 'absolute'
so layout between yourcomponent and view is free from backgroundimage.
please advise about my mistake and better component!
I'm trying to draw a dashed path, or at least give that illusion, using Framer Motion. Think animating a foot path on a treasure map. Animating the path length seems to be a common method, and so I've implemented it like below.
<motion.span
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
d="...a list of coordinates"
stroke="#000"
strokeWidth="5"
strokeDasharray="8"
/>
But it appears animating the path length doesn't work well with strokeDasharray. When I add the strokeDasharray value using the attribute, the path length animates but the strokeDasharray value, when inspected, reads 2000px instead of 8px. And when I add the strokeDasharray using CSS or inline styling, the path is dashed correctly, but the animation doesn't work.
From what I've read, strokeDasharray uses the path length when doing it's computations, so I'm guessing the initial "0" value is throwing things off. Might be way off. I don't know.
Is there a simple fix here? Or should I reassess how I go about the animation? Thank you!
Not a solution using Framer Motion, but found this pen by Ruskinz that does the job using some css animation. The HTML looks like this:
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="340" height="333" viewBox="0 0 340 333">
<defs>
<path id="path1" d="M66.039,133.545c0,0-21-57,18-67s49-4,65,8s30,41,53,27s66,4,58,32s-5,44,18,57s22,46,0,45s-54-40-68-16s-40,88-83,48s11-61-11-80s-79-7-70-41 C46.039,146.545,53.039,128.545,66.039,133.545z" />
<mask id="mask1"><use class="mask" xlink:href="#path1"></mask>
</defs>
<use class="paths" xlink:href="#path1" mask="url(#mask1)" />
</svg>
And the CSS looks like this:
.paths {
fill: none;
stroke: grey;
stroke-dasharray: 5;
stroke-width: 5;
stroke-linejoin: round;
}
.mask {
fill: none;
stroke: white;
stroke-width: 10;
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: dash 5s linear alternate infinite;
}
/* does not work in IE, need JS to animate there */
#keyframes dash {
from {
stroke-dashoffset: 1000;
}
to {
stroke-dashoffset: 0;
}
}
See the full pen at https://codepen.io/elliz/pen/prYqwx
I had the same issue because i wanted to animate it with framer-motion and not in css. What i just did is i just put an exact copy of the path with the dashedArray line below the path which i'm going to animate.
It will act as an overlay. I just gave it the stroke color of the background and tweaked the stroke-width. I don't know how it would be with a linearGradient background. But in my case with a static background color it worked.
import { motion } from 'framer-motion';
export default function Path({ pathColor, bg }) {
return (
<svg
width="245.24878"
height="233.49042"
viewBox="0 0 64.888737 61.777671"
version="1.1"
id="svg1033">
<defs
id="defs1030" />
<g
id="layer1"
transform="translate(-20.472293,-22.027827)">
<g
id="g484"
transform="translate(11.886667,6.306109)"
>
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{
pathLength: { delay: 0.4, type: "tween", duration: 3, bounce: 0 }
}}
stroke={pathColor}
strokeDasharray='3.846, 1.282'
strokeDashoffset='0'
strokeWidth='0.641'
style={{ fill: 'none', fillRule: 'evenodd', strokeLinejoin: 'round' }}
d="m 70.258127,15.782623 c 0,0 -1.867161,10.194243 -5.854843,12.473363 -9.471023,5.413069 -22.204956,-6.41444 -32.583479,-3.054701 -9.553598,3.092694 -21.015474,9.948708 -22.6557013,19.855557 -1.7758628,10.726077 5.8258513,25.311914 16.2917403,28.255989 11.258271,3.166974 19.313188,-18.990719 30.80157,-16.800859 5.208004,0.992724 10.182339,12.218805 10.182339,12.218805"
id="path1154"
/>
<path
stroke={bg}
strokeDasharray='3.846, 2.282'
strokeDashoffset='0'
strokeWidth='1.641'
style={{ fill: 'none', fillRule: 'evenodd', strokeLinejoin: 'round' }}
d="m 70.258127,15.782623 c 0,0 -1.867161,10.194243 -5.854843,12.473363 -9.471023,5.413069 -22.204956,-6.41444 -32.583479,-3.054701 -9.553598,3.092694 -21.015474,9.948708 -22.6557013,19.855557 -1.7758628,10.726077 5.8258513,25.311914 16.2917403,28.255989 11.258271,3.166974 19.313188,-18.990719 30.80157,-16.800859 5.208004,0.992724 10.182339,12.218805 10.182339,12.218805"
id="path1155"
/>
</g>
</g>
</svg>
);
}
I have this code that you can run below in the snippet. My issue is that in my production environment I have 2 SVGs, one on top of the other, each containing all sorts of content. When the user clicks on either side of the header, as seen in the snippet, the SVG that is on top should grow or shrink to either show more or less of the SVG underneath (I know this isn't perhaps performant with regards to the SVG potentially being drawn beneath - but my question likely involves this, as the answer probably has something to do with a lack of understanding of SVG viewbox on my part).
The issue is that because I want the images contents to remain the same size as the SVG on top grows or shrinks I decided to use Greensock in order to animate the grow/shrink of both the width AND the viewbox. This though causes very nasty glitching - in the snippet, run the code and click the Black box (pay attention especially to the bottom left corner of the image, but also to the jumps in the leftmost circle)!
Am I mistaken in changing both? Is there a way to only alter the width? Would this still cause the glitch as seen below?
Appreciate your help!
class Header extends React.Component {
constructor(props) {
super(props);
this.leftPanel = null;
this.leftPanelTween = null;
this.rightPanel = null;
this.rightPanelTween = null;
}
slideLeftHandler = () => {
this.leftPanelTween = TweenMax.to(this.leftPanel, 2, {width: '100%', attr: {viewBox: "0 0 500 150"}});
};
slideRightHandler = () => {
this.rightPanelTween = TweenMax.to(this.leftPanel, 2, {width: 0, attr: {viewBox: "0 0 0 150"}});
};
render() {
return (
<div class="Header">
<svg width="100%" viewBox="0 0 500 150" ref={el => this.rightPanel = el} onClick={this.slideRightHandler}>
<rect id = "middle" width="100%" height="100%" fill="black">
</rect>
<circle cx="400" cy="75" r="25" fill="red">
</circle>
<circle cx="100" cy="75" r="25" fill="red">
</circle>
</svg>
<svg width="50%" viewBox="0 0 250 150" ref={el => this.leftPanel = el} onClick={this.slideLeftHandler}>
<rect id = "middle" width="100%" height="100%" fill="red">
</rect>
<circle cx="100" cy="75" r="25" fill="black">
</circle>
<circle cx="400" cy="75" r="25" fill="black">
</circle>
</svg>
</div>
);
}
}
ReactDOM.render(
<Header />,
document.getElementById('app')
);
.Header {
margin: 0;
cursor: pointer;
}
.Header svg {
position: absolute;
}
.Header .leftPanel {
top: 0;
left: 0;
}
.Header .rightPanel {
top: 0;
right: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.2/TweenMax.min.js"></script>
<div id="app"></div>
Animating the viewBox is not performant. I recommend making the SVG as large as it needs to be at the end all of the time and just animate the width of the rectangle within the SVG that was clicked.
I have an SVG I'm using as an <img> tag. Using Styled Components I am trying to get to a point where I change the stroke color upon hover.
I imported the SVG:
import BurgerOpenSvg from '../../images/burger_open.svg';
I Created a Styled Components for it:
const BurgerImageStyle = styled.img`
&:hover {
.st0 {
stroke: red;
}
}
`;
And I use it:
<BurgerImageStyle alt="my-burger" src={BurgerOpenSvg}/>
The result is, my SVG is displayed correctly, but no color change upon hovering.
Source for the SVG I use:
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 38 28.4" style="enable-background:new 0 0 38 28.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#221f1f;stroke-width:2;stroke-miterlimit:10;}
</style>
<g>
<g id="XMLID_7_">
<line class="st0" x1="0" y1="1" x2="38" y2="1"/>
</g>
<g id="XMLID_6_">
<line class="st0" x1="0" y1="14.2" x2="38" y2="14.2"/>
</g>
<g id="XMLID_5_">
<line class="st0" x1="0" y1="27.4" x2="38" y2="27.4"/>
</g>
</g>
</svg>
The SVG Renders as follows:
Is it even possible to update the class on an SVG loaded in an <img> tag? or must it be inline <svg> tag?
So I looked into this. Turns out you cannot CSS style an SVG image you're loading using the <img> tag.
What I've done is this:
I inlined my SVG like this:
<BurgerImageStyle x="0px" y="0px" viewBox="0 0 38 28.4">
<line x1="0" y1="1" x2="38" y2="1"/>
<line x1="0" y1="14.2" x2="38" y2="14.2"/>
<line x1="0" y1="27.4" x2="38" y2="27.4"/>
</BurgerImageStyle>
Then I used Styled Components to style BurgerImageStyle:
const BurgerImageStyle = styled.svg`
line {
stroke: black;
}
&:hover {
line {
stroke: purple;
}
}
`;
This worked.
If you are looking to avoid writing separate components or copying your raw SVG file, consider react-inlinesvg;
https://github.com/gilbarbara/react-inlinesvg
import React from "react";
import styled from "styled-components";
import SVG from "react-inlinesvg";
import radio from "./radio.svg";
interface SVGProps {
color: string;
}
const StyledSVG = styled(SVG)<SVGProps>`
width: 24px;
height: 24px;
& path {
fill: ${({ color }) => color};
}
`;
export default function App() {
const color = "#007bff";
return <StyledSVG color={color} src={radio} />;
}
Code Sandbox: https://codesandbox.io/s/stack-56692784-styling-svgs-iz3dc?file=/src/App.tsx:0-414
If you want to have some styling shared across multiple SVGs and you don't want to have an extra dependency on react-inlinesvg you can use this thing instead:
In src prop it accepts SVG React component
import styled from 'styled-components';
import React, { FC, memo } from 'react';
type StyledIconProps = {
checked?: boolean;
};
const StyledIconWrapper = styled.div<StyledIconProps>`
& svg {
color: ${(p) => p.checked ? '#8761DB' : '#A1AAB9'};
transition: 0.1s color ease-out;
}
`;
export const StyledIcon = memo((props: StyledIconProps & { src: FC }) => {
const { src, ...rest } = props;
const Icon = src;
return (
<StyledIconWrapper {...rest}>
<Icon/>
</StyledIconWrapper>
);
});
And then you can use it like:
import { StyledIcon } from 'src/StyledIcon';
import { ReactComponent as Icon } from 'assets/icon.svg';
const A = () => (<StyledIcon src={Icon} checked={false} />)
In addition to what JasonGenX I propose the next case when you're using a SVG component (like one generated using SVGR). This is even on the styled-components documentation and in combination with its API it solves it seamlessly.
First import your icon
import React from 'react';
import styled from 'styled-components';
import YourIcon from '../../icons/YourIcon';
In my case I added a styled button like so:
const StyledButton = styled.button`
...
`;
// Provide a styled component from YourIcon
// You can also change the line for path and stroke for fill for instance
const StyledIcon = styled(YourIcon)`
${StyledButton}:hover & line {
stroke: #db632e;
}
`;
const YourButton = () => {
return (
<StyledButton>
<StyledIcon /> Click me
</StyledButton>
);
};
export default YourButton;
After that you'll see your icon changes its color.
Looking at the component docs, react components has three items in their lifecycle: mount, unmount and update. React transition groups seems to be the most common way to apply transitions and animations in react. Can it be used on update aswell, (ie statechange) or only when an item is mounted/unmounted?
Yes you can add transition on state change. You need to provide a key to the child element which will change on state update.
From docs :
You must provide the key attribute for all children of ReactCSSTransitionGroup, even when only rendering a single item. This is how React will determine which children have entered, left, or stayed.
Thus, you can do something like this :
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
handleClick(e){
this.setState({number: this.state.number + 1});
}
render(){
return (
<div className='container'>
<CSSTransitionGroup transitionName="example" transitionAppear={true} transitionAppearTimeout={500} transitionEnterTimeout={500} transitionLeaveTimeout={300}>
<div className="number" key={this.state.number}>{this.state.number}</div>
</CSSTransitionGroup>
<button onClick={this.handleClick.bind(this)}>Click Me!</button>
</div>
)
}
}
React.render(<Container />, document.getElementById('container'));
Css
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
.example-appear {
opacity: 0.01;
}
.example-appear.example-appear-active {
opacity: 1;
transition: opacity .5s ease-in;
}
Here is fiddle.