CSS Ripple effect with pseudo-element causing reflow - css

I'm trying to create the material ripple effect with styled-components (which is unable to import the material web-components mixins). I want to stick with using the after element for the foreground effect, to keep the accesibility tree intact.
However, most notably on mobile, the ripple transition is causing reflow in the button's content. It would seem to happen because of the display change (from none to block), but I have tried some alternatives which don't share this artifact, and this side-effect is still present.
Here's my code (I'm using some props to set the ripple, but you can hard-set them if you want to reproduce): [Here was an outdated version of the code]
Thanks for the attention.
Edit: The bug only happens when I add a hover effect to the button, very weird. Below follows the link and a code sample (you will have to set a react repository in order to reproduce it, unfortunately)
https://github.com/Eduardogbg/ripple-hover-reflow-bug
import React, { useRef, useReducer } from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components'
const ButtonBase = styled.button`
cursor: pointer;
width: 250px;
height: 6vh;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
outline: none;
position: relative;
overflow: hidden;
border-width: 0;
background-color: cyan;
:hover {
filter: brightness(1.06);
}
::after {
content: '';
pointer-events: none;
width: ${({ ripple }) => ripple.size}px;
height: ${({ ripple }) => ripple.size}px;
display: none;
position: absolute;
left: ${({ ripple }) => ripple.x}px;
top: ${({ ripple }) => ripple.y}px;
border-radius: 50%;
background-color: ${({ ripple }) => ripple.color};
opacity: 0;
animation: ripple ${({ ripple }) => ripple.duration}ms;
}
:focus:not(:active)::after {
display: block;
}
#keyframes ripple {
from {
opacity: 0.75;
transform: scale(0);
}
to {
opacity: 0;
transform: scale(2);
}
}
`
const rippleReducer = ref => (ripple, event) => {
const { x, y, width, height } = ref.current.getBoundingClientRect()
const size = Math.max(width, height)
return {
...ripple,
size,
x: event.pageX - x - size / 2,
y: event.pageY - y - size / 2
}
}
const DEFAULT_RIPPLE = {
size: 0,
x: 0,
y: 0,
color: 'white',
duration: 850
}
const Button = props => {
const ref = useRef(null)
const [ripple, dispatch] = useReducer(
rippleReducer(ref),
{ ...DEFAULT_RIPPLE, ...props.ripple }
)
return (
<ButtonBase
ref={ref}
className={props.className}
ripple={ripple}
onClick={event => {
event.persist()
dispatch(event)
}}
>
{props.children}
</ButtonBase>
)
}
ReactDOM.render(
<div style={{
backgroundColor: 'red',
width: '500px', height: '500px',
display: 'grid',
placeItems: 'center'
}}>
<Button>
<span style={{ fontSize: '30px' }}>
abacabadabaca
</span>
</Button>
</div>,
document.getElementById('root')
);

The problem seems to be related to this chromium bug that was supposedly solved a few years ago: Image moves on hover when changing filter in chrome
Setting transform: translate3d(0,0,0); looks like a fix, though my eye isn't pixel-perfect.

Related

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

Styled-components - Over 200 classes were generated, but cant get :hover to work

I came across the error in styled-components :
Over 200 classes were generated for component......
and did the suggested fix from console, and that did the trick, but when I have a container component "Card" that when hovered should change text color of another component "Number" (which has that suggested fix applied, then I cant change the color (i assume because style overrides the hover change, because it works fine with opacity)
the mentioned components are in src/ProgressPieCard (first 2 components)
anyone got any got suggestions, thanks in advance :)
( sorry styling/position is a bit off )
CodeSandBox
const Number = styled.p.attrs<ColorProps>((props) => ({
style: {
color: props.color,
},
}))`
position: absolute;
span {
font-size: 1.5rem;
}
`;
const Card = styled.div.attrs<ColorProps>((props) => ({
style: {
background: props.color,
},
}))`
position: relative;
&:hover {
${Number} {
opacity: 0.5;
// color: red; <-- this dont work
}
}
`;
Values ​​from props were pass as inline styles. They have higher priority. I suggest passing values ​​from props differently. The example below will now work as you wanted.
const Number = styled.p<ColorProps>`
position: absolute;
color: ${p => p.color};
span {
font-size: 1.5rem;
}
`;
const Card = styled.div<ColorProps>`
position: relative;
background: ${p => p.color};
&:hover ${Number} {
opacity: 0.5;
color: red; <-- this WILL work :)
}
`;

How can I better animate a modal using CSS?

I am trying to use transitions to change the way a modal I created is shown on the screen. I need the modal to slide in from the left but the code doesn't seem to work. The app is in React.
The JSX code
const Modal = (props)=>{
let ModalClasses = [Styles.Modal];
if(props.show){
ModalClasses = [Styles.Modal, Styles.Open]
}
return(
<div className={ModalClasses.join(' ')}>
<div className={Styles.ModalNav} onClick={props.clicked}>
<div></div>
<div></div>
</div>
</div>
)
}
CSS
.Modal{
background-color: white;
position: absolute;
top: 0;
left: 25%;
width: 50vw;
height: 100vh;
z-index: 2;
transform: translateX(-100%);
transition: transform 500ms ease-out;
}
.Modal.Open{
transform: translateX(0);
}
.ModalNav div{
height: 3px;
width: 20px;
background-color: black;
margin: 5px;
position: absolute;
top: 10px;
right: 0;
cursor: pointer;
}
.ModalNav div:first-of-type{
transform: rotate(45deg);
}
.ModalNav div:last-of-type{
transform: rotate(-45deg);
}
JSX code for the component I am receiving props from
class App extends Component{
state={
showModal: false,
}
showModalHandler = ()=>{
this.setState({showModal:!this.state.showModal})
}
render(){
return (
<div className="App">
{ this.state.showModal ?
<Modal
show={this.state.showModal}
clicked={this.showModalHandler}/> : null}
{ this.state.showModal ?
<Backdrop
clicked={this.showModalHandler}/> : null}
<Table clicked={this.showModalHandler}/>
</div>
);
}
}
export default App;
My goal is to get the modal to slide in but it just pops right in.
https://salesruby.netlify.app/ can be viewed here.
Here's an alternative answer that is more in line with OP's original approach of using CSS transition instead of animating. This also let's you slide the menu back in so it slides both ways.
OP's problem is returning null in the App component if the modal is in a hidden state (which it starts with). So react will not render the modal, until you change the isOpen flag to true - at that point it renders directly in the open state (popping in), and the CSS transition has no effect. Subsequently when you close the modal, it is removed from the DOM, again without time for a transition so it "pops out".
The solution below is to render the modal in its starting state, and use the IsOpen flag to toggle the state of the element, rather than pulling it in and out of the DOM.
Here is a modified sandbox from Cyrus' answer that shows this - isModalOpen flag is used by the Modal to toggle the "open" class. The rest is basically the same as what OP started with.
https://codesandbox.io/s/bold-architecture-9t50w?file=/src/Components/Modal/Modal.js
App (Notice Modal is always returned, no : null - and isModalOpen is bound)
import React, { useState } from "react";
import "./styles.css";
import Modal from "./Components/Modal/Modal";
const App = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setIsModalOpen(true)}>Open modal </button>
<Modal isModalOpen={isModalOpen} closeModal={setIsModalOpen} />
</div>
);
};
export default App;
Modal (Notice className={isModalOpen ? "open" : ""} to toggle class)
import React from "react";
import "./modal.css";
const Modal = ({ closeModal, isModalOpen }) => {
return (
<div id="modal" className={isModalOpen ? "open" : ""}>
Modal here <button onClick={() => closeModal(false)}> open modal</button>
</div>
);
};
export default Modal;
CSS (simple transform and transition effect)
#modal {
background-color: #000000cc;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transform: translateX(-100%);
transition: transform 0.5s ease-in-out;
width: 30%;
height: 100%;
}
#modal.open {
transform: translateX(0);
}
so I made this really fast. This is just a demo on how to make the animation slide in from left to right. If you look at the css you can see that I have left : -600px and that is the width of the modal. You can make it dynamic so that it will only take some % of the screen, but don't forget to add a max-width.
here is a link to codesandbox
//app.js
import React, { useState } from "react";
import "./styles.css";
import Modal from "./Components/Modal/Modal";
const App = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setIsModalOpen(true)}>Open modal </button>
{isModalOpen ? <Modal closeModal={setIsModalOpen} /> : null}
</div>
);
};
export default App;
//Modal.js
import React, { useEffect } from "react";
import "./modal.css";
const Modal = ({ closeModal }) => {
return (
<div id="modal">
Modal here <button onClick={() => closeModal(false)}> open modal</button>
</div>
);
};
export default Modal;
//modal.css
#modal {
background-color: #000000cc;
position: absolute;
top: 0;
left: 100px;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transition: top 2s;
animation: slideIn 1s;
width: 600px;
height: 600px;
}
#keyframes slideIn {
0% {
left: -600px;
}
100% {
left: 100px;
}
}

How to hold a CSS animation

I currently am making a toast/snackbar via React and CSS. I have the snackbar opening, and closing, however, not sure what to add in order to hold the animation for a few seconds? The 'fade-in' property acts as a default state, where as 'fade-in-show' acts as the animation.
ReactJS
import React, { Component } from 'react';
import './styles.css';
class Toaster extends Component {
componentDidMount = () => {
setTimeout(() => this.setState({ fade: true }), 100);
setTimeout(() => this.setState({ fade: false }), 3000);
};
state = {
fade: false,
};
render() {
const { status, children } = this.props;
const styles = {
box: {
position: 'absolute',
top: 20,
right: 0,
display: 'block',
background: 'green',
marginBottom: '1em',
zIndex: 9999,
},
position: {
position: 'absolute',
alignItems: 'center',
top: '18%',
paddingLeft: '10px',
},
};
if (children) {
return (
<div>
<div
style={styles.box}
id={this.state.fade ? 'fade-in-show' : 'fade-in'}
>
<div style={styles.position}>{children}</div>
</div>
</div>
);
}
return null;
}
}
export default Toaster;
CSS
#fade-in {
height: 50px;
width: 1px;
opacity: 0;
transition: all 2.2s ease;
}
#fade-in-show {
opacity: 1;
height: 50px;
width: 500px;
transition: all 2.2s ease;
}
As far as CSS goes you can delay the transition property by adding a delay value before the duration value. Here's an example if you wanted to delay for five seconds...
transition: all 5s 2.2s ease

Resources