I want to animate an element when the user clicks on a button.
The element displays a count of how many times the button has been clicked. This is managed through a state that is incremented on every button click.
Whenever the button is clicked, the displayed number should bounce/animate, to emphasize its change.
In essence I want to conditionally append a CSS class. There's many examples of how to do this, but almost all of them only have a single class applied to them, not lots of classes.
I use tailwindcss for styling. This means lots of CSS classes on lots of elements.
What I have considered
String concatenation
Having a classNames array, where the animation class can be added and removed easily.
const [count, setCount] = useState(0);
const [classNames, setClassNames] = useState([
'text-2xl', 'mb-2', 'font-bold', 'text-black', 'dark:text-white'
]);
const countElement = <p className={classNames.join(' ')}>{count}</p>;
const handleClick = () => {
setCount(count + 1);
// Append the animation class.
setClassNames(classNames.concat('animate-bounce'));
// Remove the animation class after the animation is done.
setTimeout(() => {
setClassNames(classNames.filter(className =>
className !== 'animate-bounce'));
}, 1000);
};
return <>
{count > 0 ? countElement : null}
<button className="px-4 py-3 mb-2 rounded-lg font-semibold transition-colors duration-300
bg-cardinal-default hover:bg-cardinal-dark active:bg-cardinal-darkest
text-black dark:text-white"
onClick={handleClick}>Click me</button>
</>;
Though with tailwindcss this is kind of ugly as it separates all of the styling from its actual element. Not exactly ideal. It feels very hacky.
Animate an inner element
const [isBouncing, setIsBouncing] = useState(false);
const countElement = (
<p className="text-2xl mb-2 font-bold text-black dark:text-white">
<span className={isBouncing ? 'inline-block animate-spin' : ''}>{count}</span>
</p>
);
...
setCount(count + 1);
setIsBouncing(true);
setTimeout(() => setIsBouncing(false), 1000);
...
This is better, yet it still requires me to set inline-block
Is there a better way of doing this? Or have I exhausted all worthwhile solutions?
You could use a css-in-js solution like styled-components which will make it really easy to enable/disable styles.
For tailwind there is a library to help with css-in-js called twin
This allows you to conditionally enable styles:
const Button = styled.button`
${tw`your tailwind styles here`}
${props => props.shouldAnimate ? `some animation css here` : "" }
`
TailwindCSS is a utility first CSS Framework, so there's probably no other option other than appending classes to your elements.
If you were to use an animation library, like Framer Motion, it wouldn't be necessary to append Tailwind classes. On the other hand, you would need to create the animation yourself, which is quite easy.
Regarding your considerations, the first alternative seems indeed very hacky, whereas the second one doesn't and should work as well.
Nonetheless, it's up to you to decide which method suits you best.
Related
Here I created a sample example. As you can see I add applyColor Style object to apply styles.
import React from 'react'
const applyColor = {
color: 'red'
}
export const App = () => {
const renderData = () => {
return (
<>
<section style={applyColor}>
<p>Paragraph</p>
<div>
<h2>Heading</h2>
<span>Span</span>
</div>
</section>
</>
)
}
return <>
{renderData()}
</>
}
How can I modify applyColor Object to access the child from parent element <section />?
And I dont want to use any third party like styled-components and material-ui/styles.
Thanks:)
In short, no way unless you do it directly to the child.
<div style={{ color: red }} />
That's what the style is made for. However, everything is coded as Javascript code, so you can do it manually via props.
return <Child color="red" />
const Child = ({ color }) => {
return <div style={{ color }} />
}
Every big UI system has their unique way of handling this, ex. Material and etc.
But trusted me, unless you are making small css changes, styled-components like Css-in-Js approach isn't avoidable. Because passing the flag from Parent to Child is just too much work to manage by your own, not difficult but tedious. This is just my two cents.
Or you can use a traditional CSS approach, just put everything through CSS class. This way you can just include one copy of css. But the problem of that approach is that you can't pass a flag into the CSS so in the end you come back to the same boat above :)
I am trying to create a Design System using ReactJS and TailwindCSS.
I created a default Button component with basic styling as follow:
import React from "react";
import classNames from "classnames";
const Button = React.forwardRef(
({ children, className = "", onClick }, ref) => {
const buttonClasses = classNames(
className,
"w-24 py-3 bg-red-500 text-white font-bold rounded-full"
);
const commonProps = {
className: buttonClasses,
onClick,
ref
};
return React.createElement(
"button",
{ ...commonProps, type: "button" },
children
);
}
);
export default Button;
I then use the Button in my page like:
import Button from "../src/components/Button";
export default function IndexPage() {
return (
<div>
<Button onClick={() => console.log("TODO")}>Vanilla Button</Button>
<div className="h-2" />
<Button
className="w-6 py-2 bg-blue-500 rounded-sm"
onClick={() => console.log("TODO")}
>
Custom Button
</Button>
</div>
);
}
This is what is displayed:
Some attributes are overridden like the background-color but some aren't (the rest).
The reason is the classes provided by TailwindCSS are written in an order where bg-blue-500 is placed after bg-red-500, therefore overriding it. On the other hand, the other classes provided in the custom button are written before the classes on the base button, therefore not overriding the styles.
This behavior is happening with TailwindCSS but might occurs with any other styling approach as far as the class order can produce this scenario.
Do you have any workaround / solution to enable this kind of customisation?
Here is a full CodeSanbox if needed.
One approach is to extract classes from your component using Tailwind's #apply in your components layer.
/* main.css */
#layer components {
.base-button {
#apply w-24 py-3 bg-red-500 text-white font-bold rounded-full;
}
}
// Button.js
const Button = React.forwardRef(({ children, className = "", onClick }, ref) => {
const buttonClasses = classNames("base-button", className);
// ...
);
This will extract the styles into the new base-button class, meaning they can easily be overwritten by the utility classes you pass to the Button component.
Another approach to create reusable React components using Tailwind is as follows..
Read this gist
https://gist.github.com/RobinMalfait/490a0560a7cfde985d435ad93f8094c5
for an excellent example.
Avoid using className as a prop. Otherwise, it'd be difficult for you to know what state your component is in. If you want to add an extra class, you can easily extend.
You need a helper for combining classname strings conditionally. Robert, the writer of this gist, shared the helper function also with us:
export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
}
To have Tailwind CSS override material theming (or something else for that matter) one could apply !important to all tailwind utilities with configuration to module.exports.
The important option lets you control whether or not Tailwind’s utilities should be marked with !important. This can be really useful when using Tailwind with existing CSS that has high specificity selectors.
To generate utilities as !important, set the important key in your configuration options to true:
tailwind.config.js
module.exports = {
important: true
}
https://tailwindcss.com/docs/configuration#important
To solve, I recommend doing what Bootstrap does. Use a default class for your default button like:
.button {
width: 2rem;
background-color: red;
border-radius: 0.25rem;
}
Then when customizing a button you should apply classes that either come after the button class in your CSS file, or come in a different CSS file that is called after your default CSS file, or use the !important declaration.
Old answer
Use your browser developer tools to observe how your browser is loading CSS styles on an element. For example, in Chrome, right-click on the custom button and select "Inspect". A DevTools window will open and the element will be highlighted in the DOM.
On the right, you should have a Styles pane. There, you'll see a list of all the CSS styles being applied to the element. Styles with strikethroughs are being overridden by styles called by other CSS classes or inline styles.
In your case, the custom button has both the "CommonProps" classes and the classes you're adding in IndexPage. For example, both class w-6 and class w-24.
Class w-24 is overriding class w-6 because of CSS precedence. Read more about CSS precedence here. Check out rule #3 in the accepted answer. I think that's what's happening to you.
To solve, you may want to remove some classes from commonProps. Or use the !important declaration on some classes. This is the part of your design system that you need to think through. Look at how other systems like Bootstrap have done it.
const handleMenu = async () => {
let menu = await document.querySelector('.navbar-list');
menu.classList.toggle('.navbar-list-active');
}
return (
<div className="navbar-list">
<div onClick={handleMenu} className="hamburger">
</div>
</div>
)
The thing is that after class is added to component it really added on the element, but the CSS styling doesn't appear on the element with added class, is this something React is doing? How can I force CSS to re-render the page?
await doesn't do anything useful unless the value you are awaiting is a promise. querySelector returns either null or an element: never a promise. Your code won't wait for the element to exist.
Now, you can use useRef to get a reference to the element which would then let you toggle the class on it… but don't.
The React way to solve this problem is to store the toggled state in the state and deal with it in the JSX.
const MyComponent = () => {
const [active, setActive] = useState(false);
const classNames = ["navbar-list"];
if (active) classNames.push("navbar-list-active");
const handleMenu = () => setActive(!active);
return (
<div className={classNames}>
<button onClick={handleMenu} className="hamburger">
Toggle Menu
</button>
</div>
;)
}
Note that I've changed the div to a button. This is an important accessibility feature and allows the menu to be activated by people who don't use a pointing device to access a page. There is more you should do for accessibility and this article is a good starting point (although it uses traditional DOM instead of React so you'll need to adapt it to use state as I did here).
I have this class.
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.add = this.add.bind(this);
this.clear = this.clear.bind(this);
}
add() {
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
}
clear() {
this.setState({ items: [] });
}
render() {
return (
<div>
<div>
<button onClick={this.add}>Add</button>
<button onClick={this.clear}>Clear</button>
</div>
{/* This is wrong, not sure what to do though... */}
<Collapse in={this.state.items.length > 0}>
<ul>{this.state.items}</ul>
</Collapse>
</div>
);
}
}
Sandbox link: https://codesandbox.io/s/material-demo-ggv04?file=/Demo.js
I'm trying to make it so that every time I click the "add" button, a new item gets animated into existence at the top of the list and the existing items get pushed down. Not sure how to proceed though.
Extra Resources
Example of what I'm trying to achieve: https://codeburst.io/yet-another-to-do-list-app-this-time-with-react-transition-group-7d2d1cdf37fd
React Transition Group Transition docs: http://reactcommunity.org/react-transition-group/transition (which seem to be used internally by Collapse)
I updated your Sandbox code to achieve what you wanted, but I don't think MaterialUI is the best library for that (I could be missing a better way to do it).
The challenge is that when you add a new item, that doesn't exist in the DOM yet. And most of those animation libraries/components require the element to be in the DOM and they just "hide" and "show" it with a transition time.
I had a similar situation and after some research, the better library I found that can handle animation for elements that are not yet in the DOM, was the Framer Motion. (You can check their documentation for mount animations)
Anyway, here is the link for the new Code Sandbox so you can take a look. The changes I made:
Removed random key
In the map function that creates your list using the <Collapse /> component, there was a function to get a random integer and assign that as a key to your component. React needs to have consistent keys to properly do its pretenders, so removing that random number fixes the issue where your "Toggle" button wasn't animating properly. (If your list of items doesn't have an unique ID, just use the index of the map function, which is not a good solution, but still better than random numbers).
<Collapse key={i} timeout={this.state.collapseTimeout} in={this.state.open}>
{it}
</Collapse>
Added a new function to control the toggle
The approach here was: add the item in your list and, after the element is in the DOM, close the <Collapse />, wait a little bit and open it again (so you can visually see the animation). In order to do that, we needed a new "toggle" function that can explicit set the value of the collapse.
toggleValue(value) {
this.setState(() => {
return {
open: value
};
});
}
Added a variable timeout for the collapse
The last issue was that, closing the <Collapse /> when the new item is added, was triggering the animation to close it. The solution here was to dynamically change the timeout of the collapse, so you don't see that.
setCollapseTimeout(value) {
this.setState(() => {
return {
collapseTimeout: value
};
});
}
When adding the element to the list, wait to trigger the animation
Again, to work around the issue with elements not yet in the DOM, we need to use a setTimeout or something to wait to toggle the <Collapse />. That was added in your add() function.
add() {
this.toggleValue(false);
this.setCollapseTimeout(0);
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
setTimeout(() => {
this.setCollapseTimeout(300);
this.toggleValue(true);
}, 100);
}
Again, this is a hacky solution to make <Collapse /> from MaterialUI work with elements that are not yet in the DOM. But, as mentioned, there are other libraries better for that.
Good luck :)
Ended up here earlier on and then came back to create a sandbox showing hopefully a simple method for this scenario. The material-ui docs are a bit (lot) light in this area and I was fighting with a very similar situation, but I tried something with TransitionGroup from react-transition-group, crossed my fingers and it seemed to work.
Forked CodeSandbox with TransitionGroup
The gist is that you
wrap all of the components you want to transition in the <TransitionGroup> component
Inside the TransitionGroup, put in the "condition" (logic or loop output) for the data you want to render
Wrap the individual components you want to transition with transition component of your choice - <Collapse> in this example
e.g. In its most simple setup where "items" is an array of unique numbers coming from either props, state or a redux store
<TransitionGroup>
{items.map(item => (
<Collapse key={item}>
I am item {item}
</Collapse>
))}
</TransitionGroup>
With this setup I have found that I didn't need to put any props on the TransitionGroup or Collapse, and the TransitionGroup handled all the mounting and unmounting in the loop rendering. Material UI doesn't produce the lightest of HTML output, but I guess it's all rendered on the fly so maybe that makes it better (unless you have thousands of elements, then things start to drag).
You can even go a step further and wrap the whole thing in another TransitionGroup to cover situations where you want to remove the whole thing without transitioning all of the individual items - in this instance I switched it to a <Slide>. I was absolutely certain that this wouldn't work, but it seemed to not care. You can also try and be semantic and use the "component" property rather than wrapping in another element e.g.
{items.length > 0 && (
<TransitionGroup>
<Slide>
<TransitionGroup component="ul">
{items.map((item) => (
<Collapse component="li" key={item}>I am item {item}</Collapse>
))}
</TransitionGroup>
</Slide>
</TransitionGroup>
)}
I have changed the sandbox in the following ways
Included TransitionGroup from react-transition-group
Changed the "add" logic so that the components aren't part of the "items" array - the array only contains the data required to render the components
I have added a simple "count" and pushed that to the array to give the items a unique index (had originally used Math.random, but I wanted a "prettier" output). Generally your items will probably be coming from a database somewhere where a unique id will already be set.
Rendered the components in a loop based on the data in the array (this could be done in a separate function, but the gist is that the components aren't being stored in the array)
added a "delete" function to show the removal of single items
wrapped the whole group in a second <TransitionGroup> to show that the unmounting can happen in a group level
Put in some simple styling to get a better idea of the effect. You could use Material UI components here, but just wanted to keep it simple.
Hope this helps someone in the future.
I am using ReactCSSTransitionGroup to do some animation and I found an interesting thing which does not make any sense to me.
In the example below, when I click <div className="HeartControl">, it will update the height of the <div className="HeartFill"> which works fine. (I know to achieve the effect does not necessarily need ReactCSSTransitionGroup here though).
Interesting thing is that when I click, there will be another <div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div> with a new React component id added after the existing one.
But I expect there will always be only ONE <div className="HeartFill"> there.
Why this happened???
P.S.. after a few clicks, the result will look like:
<span data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2">
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3.5999999999999996px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$6:0"></div>
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$5:0"></div>
var HEIGHT_HEART = 30;
var NUM_HEART_MAX = 50;
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var Heart = React.createClass({
getInitialState: function() {
return {
heartHeight: 0
};
},
onClick: function(e) {
var currentHeartHeight = this.state.heartHeight;
this.setState({
heartHeight: currentHeartHeight + 1
});
},
render: function() {
var styleHeartFill = {
'position': 'absolute',
'bottom': 0,
'left': 0,
'width': 30,
'height': this.state.heartHeight / NUM_HEART_MAX * HEIGHT_HEART,
'background-color': '#D64541'
};
return (
<div className="Heart" >
<div className="HeartControl" onClick={this.onClick}>
<i className="fa fa-angle-up" />
</div>
<img src="heart.png" className="HeartOutline" />
<ReactCSSTransitionGroup transitionName="HeartFill">
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
</ReactCSSTransitionGroup>
</div>
);
}
});
React.renderComponent(<Heart />, document.getElementById('Heart'));
`
I suspect the reason your getting more than one is because your using the key prop
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
From React docs http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).
Heres a jsfiddle using the key prop http://jsfiddle.net/kb3gN/3826/
Heres a jsfiddle not using the key prop http://jsfiddle.net/kb3gN/3827/
P.s I've made a few changes in the fiddle just to try and better demostrate the reasoning
I'm fairly late to the game with this answer, but I ran into this issue as well and want to provide a solution for others. Removing the key is not a sufficient solution since React relies on it to know when to animate items. The documentation now has a section discouraging this.
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.
- https://facebook.github.io/react/docs/animation.html
If you are only animating the entry/exit of a single item, a CSS hack can be used to fix flickering that may be seen from multiple items entering/exiting.
.HeartFill {
display: none;
}
.HeartFill:first-child {
display: block;
}
React will add new elements on top in most cases, but this isn't guaranteed. If your transitionEndTimeout prop is set to a relatively short time, this shouldn't be a huge concern. The timeout prop should also match the CSS transition time.
Here is the problem:
You are providing a value for key which is changing over time. Keys are used to decide if an element is the same or different.
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
When you do this the value for key changes and React thinks a new element is entering and an old element is leaving.
Usually you need a unique key which can either be sequential or be generated using Math.random(). (remember to generate them once with getInitialState or DefaultProps, not in render, as that would create a new key every time).
The order of elements is another thing that can be in trouble.
From React's documentation:
In practice browsers will preserve property order except for properties that can be
parsed as a 32-bit unsigned integers. Numeric properties will be ordered sequentially
and before other properties. If this happens React will render components out of
order. This can be avoided by adding a string prefix to the key:
ReactCSSTransitionGroup should create a second element
it will remove it when a specified css animation has finished
or print a warning when there is no animation in the css
maybe look at the Low-level API for better understanding: http://facebook.github.io/react/docs/animation.html (bottom of the page)
I used gluxon's advice as a starting point - what worked for me was removing the leave transition and making it display nothing:
.example-leave.example-leave-active { display: none; }