How to hold a CSS animation - css

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

Related

CSS/Vue.js synchronize animation reset with ref value change

I want to create an animation that fades between 3 states. To store these states (that contain an image and some text), I've created an array of objects (uploadSteps). I increment the index of this array at a specific interval to create an animation.
My problem comes with CSS, I would like to create a fade-out/fade-in effect between each step, so that the transition is more smooth. It works most of the time but sometimes, after a refresh and for the first transition, I think the CSS animation is restarted before the state is changed. I wasn't able to reproduce it constantly so far.
What it does => Image fade-in -> Image 1 -> Image 1 fade-Out -> Image 1 -> Image 2 fade-in
What I want => Image fade-in -> Image 1 -> Image 1 fade-Out -> Image 2 fade-in
If you pay attention you can see that there's kind of a blink effect at the transition between image 1 and 2. What could I do to solve this ? Help appreciated !
PS: I've included some videos to illustrate my issue
https://streamable.com/4aigjq - Animation KO
https://streamable.com/umu3nj - Animation OK
<template>
<div class="screenshot-upload-processing-container">
<div class="image-container">
<img
ref="imageRef"
:src="
currentStep ? $requireImage(currentStep.imagePath).toString() : ''
"
:alt="currentStep.text"
class="image"
/>
</div>
<div class="centered-row">
<span ref="textRef" class="text">{{
currentStep ? currentStep.text : ''
}}</span>
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
onBeforeUnmount,
ref,
} from '#nuxtjs/composition-api';
export default defineComponent({
name: 'ScreenshotUploadProcessing',
setup() {
const stepNumber = ref(0);
const timer = ref<NodeJS.Timeout | null>(null);
const imageRef = ref<HTMLImageElement | null>(null);
const textRef = ref<HTMLSpanElement | null>(null);
const uploadSteps = computed(() => {
return [
{
imagePath: 'image1.svg',
text: 'Text 1',
},
{
imagePath: 'image2.svg',
text: 'Text 2',
},
{
imagePath: 'image3.svg',
text: 'Text 3',
},
];
});
const resetAnimations = () => {
const image = imageRef.value;
const text = textRef.value;
if (image) {
image.style.animation = 'none';
void image.offsetHeight;
image.style.animation = '';
}
if (text) {
text.style.animation = 'none';
void text.offsetHeight;
text.style.animation = '';
}
};
const updateStepNumber = () => {
timer.value = setInterval(() => {
if (stepNumber.value === uploadSteps.value.length - 1) {
stepNumber.value = 0;
} else {
stepNumber.value++;
}
resetAnimations();
}, 3000);
};
onBeforeUnmount(() => {
clearInterval(timer.value as NodeJS.Timeout);
});
const currentStep = computed(() => uploadSteps.value[stepNumber.value]);
updateStepNumber();
return {
stepNumber,
currentStep,
imageRef,
textRef,
uploadSteps,
};
},
});
</script>
<style lang="scss" scoped>
#keyframes fade {
0%,
100% {
opacity: 0;
}
40%,
60% {
opacity: 1;
}
}
.screenshot-upload-processing-container {
display: flex;
flex-direction: column;
height: 100%;
}
.image {
animation: fade 3s linear;
}
.image-container {
display: flex;
align-items: center;
justify-content: center;
height: 27rem;
margin-top: 10.3rem;
}
.centered-row {
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacing-xl;
}
.text {
#extend %text-gradient-1;
animation: fade 3s linear;
font-weight: $font-weight-medium;
}
</style>

React component with state triggers animation in subcomponent when the state of parent component is changed

ProjectsSection component renders a document section with header and cards. A header animation should be triggered when the header is visible on the screen for the 1st time and run only once. However when I added a state to the ProjectsSection component with every state change a header animation runs and what is surprising to me only a part of it (letters/ spans that are children to h2 move, but brackets/ pseudoelements on h2 don't). In my project I use css modules for styling.
I have tried to wrap SectionHeader component in React.memo but it does not help.
Main component:
const ProjectsSection = () => {
const [openProjectCardId, setOpenProjectCardId] = useState('');
return (
<section id="projects" className="section">
<div className="container">
<SectionHeader>Projects</SectionHeader>
<div className={styles.content_wrapper}>
{projects.map((project, ind) => (
<Project
key={project.id}
project={project}
ind={ind}
isOpen={openProjectCardId === project.id}
setOpenProjectCardId={setOpenProjectCardId}
/>
))}
</div>
</div>
</section>
);
};
SectionHeader component:
const SectionHeader = ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
);
const textToLetters = children.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{textToLetters}</h2>
</div>
);
};
Css
.sectionHeader_wrapper {
position: relative;
// other properties
&::before {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
&::after {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
}
.sectionHeader {
position: relative;
display: inline-block;
overflow: hidden;
// other properties
}
.letter {
position: relative;
display: inline-block;
transform: translateY(100%) skew(0deg, 20deg);
}
.sectionHeader__isVisible .letter {
animation: typeletter .25s ease-out forwards;
}
useIntersection hook
const useIntersection = (
activeClass,
{ root = null, rootMargin = '0px', threshold = 1 },
dependency = [],
unobserveAfterFirstIntersection = true
) => {
const elementRef = useRef(null);
useEffect(() => {
const options: IntersectionObserverInit = {
root,
rootMargin,
threshold,
};
const observer = new IntersectionObserver((entries, observerObj) => {
entries.forEach((entry) => {
if (unobserveAfterFirstIntersection) {
if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
observerObj.unobserve(entry.target);
}
} else if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
} else {
entry.target.classList.remove(activeClass);
}
});
}, options);
// if (!elementRef.current) return;
if (elementRef.current) {
observer.observe(elementRef.current);
}
}, [...dependency]);
return elementRef;
};
I have found a solution to this problem.
This was apparently about textToLetters variable created every time the state of parent component was changing.
I used useMemo to keep this value and now animation is not triggered anymore.
const textToLetters = (text) =>
text.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
const SectionHeader= ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
[children]
);
const letters = React.useMemo(() => textToLetters(children), [children]);
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{letters}</h2>
</div>
);
};

CSS Ripple effect with pseudo-element causing reflow

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.

Animate React Modal

I have a Modal in my web app which I want to slide in when the user clicks "open" button. I am using react-transition-group to achieve this. But I am unable to get the animation working. Not sure what's wrong here?
import React from 'react';
import ReactDOM from 'react-dom';
import { CSSTransition } from 'react-transition-group';
import Modal from 'react-modal';
import './styles.css';
class Example extends React.Component {
state = {
isOpen: false,
};
toggleModal = () => {
this.setState(prevState => ({
isOpen: !prevState.isOpen,
}));
};
render() {
const modalStyles = {
overlay: {
backgroundColor: '#ffffff',
},
};
return (
<div>
<button onClick={this.toggleModal}>
Open Modal
</button>
<CSSTransition
in={this.state.isOpen}
timeout={300}
classNames="dialog"
>
<Modal
isOpen={this.state.isOpen}
style={modalStyles}
>
<button onClick={this.toggleModal}>
Close Modal
</button>
<div>Hello World</div>
</Modal>
</CSSTransition>
</div>
);
}
}
CSS File:
.dialog-enter {
left: -100%;
transition: left 300ms linear;
}
.dialog-enter-active {
left: 0;
}
.dialog-exit {
left: 0;
transition: left 300ms linear;
}
.dialog-exit-active {
left: -100%;
}
Here is the working code.
The problem here is that react-modal uses a react-portal as its mounting point. So it is not rendered as the children of the CSSTransition component. So you could not use the CSSTransition with it.
You can use some custom css to create transition effects. It is in the react-modal documentation.
So if you add this to your css. There will be transition.
.ReactModal__Overlay {
opacity: 0;
transform: translateX(-100px);
transition: all 500ms ease-in-out;
}
.ReactModal__Overlay--after-open {
opacity: 1;
transform: translateX(0px);
}
.ReactModal__Overlay--before-close {
opacity: 0;
transform: translateX(-100px);
}
And we have to add the closeTimeoutMS prop to the modal in order the closing animation to work.
<Modal
closeTimeoutMS={500}
isOpen={this.state.isOpen}
style={modalStyles}
>
https://codesandbox.io/s/csstransition-component-forked-7jiwn

Elliptical Border Radius in React Native

I'm trying to recreate this shape in React Native. It's made with 2 overlapping shapes, one square and one with a border-radius on the top and bottom (the square is there to hide the border radius on the top).
Here's the CSS used to generate it:
#square {
width: 200px;
height: 180px;
background: red;
}
#tv {
position: relative;
margin-top: 100px;
width: 200px;
height: 200px;
margin: 20px 0;
background: red;
border-radius: 100px / 20px;
}
But I can't find any documentation on the border-radius property, so I don't know if it's possible to do elliptical radii like you can in CSS.
To construct an elliptical view
import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
//import { Constants } from 'expo';
export default class App extends Component {
render() {
return (
<View style={styles.container}>
<View style={styles.oval}/>
</View>
);
}
}
const styles = StyleSheet.create({
oval: {
width: 100,
height: 100,
borderRadius: 50,
//backgroundColor: 'red',
borderWidth:2,
borderColor:'black',
transform: [
{scaleX: 2}
]
},
container:{
flex:1,
alignItems:'center',
justifyContent:'center'
}
});
Playground: https://snack.expo.io/BJd-9_Fbb
This may be the shape similar to the one you have given.`
import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
//import { Constants } from 'expo';
export default class App extends Component {
render() {
return (
<View style={styles.container}>
<View style={styles.oval}/>
<View style={styles.square}/>
</View>
);
}
}
const styles = StyleSheet.create({
square:{
width:200,
height:180,
backgroundColor:'red',
position:'absolute',
top:160
},
oval: {
position:'absolute',
width: 100,
height: 200,
borderRadius: 50,
backgroundColor: 'red',
//borderWidth:2,
//borderColor:'black',
transform: [
{scaleX: 2}
]
},
container:{
flex:1,
alignItems:'center',
justifyContent:'center'
}
});
`
https://snack.expo.io/r11PnOK-Z``
EDIT
You can add different border-radius to different vertices like below
borderTopLeftRadius: '10%',
borderTopRightRadius: '30%',
borderBottomLeftRadius: '50%',
borderBottomRightRadius: '70%',
Playground: https://snack.expo.io/BJd-9_Fbb

Resources