I have a simple React component that displays some text with a 'typewriter' kind of effect via CSS. The problem I have is that this typewriter effect is re-displayed every time the component re-renders but I only want it applied on the first render and never again. Is there a way to achieve this? Below is an example...
import React from 'react';
import './Typewriter.css'
export function Blah() {
return <div className='typewriter'>
My typewriter text here
</div>
}
.typewriter {
font-family: monospace;
color:#0000;
background:
linear-gradient(-90deg, #39ff14 5px,#0000 0) 10px 0,
linear-gradient(#39ff14 0 0) 0 0;
background-size:calc(100*1ch) 200%;
-webkit-background-clip:padding-box,text;
background-clip:padding-box,text;
background-repeat:no-repeat;
animation:
b .7s infinite steps(1),
t calc(100*.01s) steps(100) forwards;
}
#keyframes t{
from {background-size:0 200%}
}
#keyframes b{
50% {background-position:0 -100%,0 0}
}
Stop the animation after the first time by using useRef and useEffect as in the following example
const { useRef, useEffect } = React;
const Example = () => {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return <div className={firstRender.current ? 'typewriter' : ''}>
My typewriter text here
</div>
}
ReactDOM.render(<Example />, document.getElementById("react"));
.typewriter {
font-family: monospace;
color:#0000;
background:
linear-gradient(-90deg, #39ff14 5px,#0000 0) 10px 0,
linear-gradient(#39ff14 0 0) 0 0;
background-size:calc(100*1ch) 200%;
-webkit-background-clip:padding-box,text;
background-clip:padding-box,text;
background-repeat:no-repeat;
animation:
b .7s infinite steps(1),
t calc(100*.01s) steps(100) forwards;
}
#keyframes t{
from {background-size:0 200%}
}
#keyframes b{
50% {background-position:0 -100%,0 0}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Related
N.B. With the (more complex) setup I'm actually working with I can't use CSS Transitions. I recognise that CSS Transitions would be a
perfectly good solution in the example below.
I'm having a little trouble with
animation-direction: reverse
which I've never used before but doesn't seem to be running the way I might have expected it to.
The easiest solution to my problem would be to write two CSS #keyframes animations and use one or the other.
But for the sake of economy and elegance I would like to use a single animation and play it forwards or backwards.
This example below shows the effect I'm trying to achieve.
When the page loads, pressing either button will fire the intended animation.
However, after one button is pushed, the animation no longer runs and only the end-frame of the forwards or reverse animation is displayed.
What am I doing wrong here?
Working Example:
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.className = 'square';
square.classList.add('outbound');
}, false);
buttonReturn.addEventListener('click', () => {
square.className = 'square';
square.classList.add('return');
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>
The browser considers that animation as complete therefore it does not restart it in order to restart the animation you need to first remove the class and then re-add it however for the browser to recognize this change you need add a slight delay. Adding a setTimeout does the trick even if the timeout is 0 because js is single threaded.
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.classList.remove('return');
square.classList.remove('outbound');
setTimeout(() => {
square.classList.add('outbound');
}, 0)
}, false);
buttonReturn.addEventListener('click', () => {
square.classList.remove('return');
square.classList.remove('outbound');
setTimeout(() => {
square.classList.add('return');
}, 0)
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>
I had a think about this away from my laptop screen and realised that... what was missing from my original set up was one additional static class, describing the presentational state of .square after the .outbound animation has run:
.square.outbounded {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
(N.B. There's no need for a corresponding static class, describing the state of .square after the .return animation has run, since the presentational state that class would describe is already described in the initial styles of .square)
Working Example (with .outbounded class added)
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.className = 'square outbound';
setTimeout(() => {
square.classList.add('outbounded');
square.classList.remove('outbound');
}, 1000);
}, false);
buttonReturn.addEventListener('click', () => {
square.className = 'square return';
setTimeout(() => {
square.classList.remove('return');
}, 1000);
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbounded {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>
In my navbar, I have a "Cart" item with a <v-badge /> on it that displays how many items are in the cart. When a user adds or removes to the cart, the number correctly increments and decrements. On state change of that number however, I'd like to be able to "bounce" the badge to provide the user with feedback that the item has been added or removed from the cart. I've been looking at the Vue docs for animations and transitions, I'm just not quite understanding how I'd go about achieving this.
I've attempted wrapping the badge in a <transition /> element and applying some keyframes animations I found on CSS Tricks, however it's still not working.
html:
<v-tabs
class="hidden-sm-and-down"
optional>
<v-tab
v-for="(item, i) in items"
:key="i"
:exact="item.title === 'Home'"
:to="item.to"
:ripple="false"
active-class="text--primary"
class="font-weight-bold nav-link"
min-width="96"
nuxt
text>
<transition
name="ballmove"
enter-active-class="bouncein"
leave-active-class="rollout">
<v-badge
v-if="item.badge && hasCartItems"
color="red"
:content="cartItems"
:value="cartItems"
class="default-badge"
overlap>
{{ item.title }}
</v-badge>
<span v-else>{{ item.title }}</span>
</transition>
</v-tab>
</v-tabs>
scss:
#mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
#keyframes bouncein {
1% { #include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { #include ballb() }
30% { #include ballb(-80px); }
50% { #include ballb(-40px); }
70% { #include ballb(-30px); }
90% { #include ballb(-15px); }
97% { #include ballb(-10px); }
}
#keyframes rollout {
0% { transform: translate3d(0, 300px, 0); }
100% { transform: translate3d(1000px, 300px, 0); }
}
#keyframes ballroll {
0% { transform: rotate(0); }
100% { transform: rotate(1000deg); }
}
.rollout {
width: 60px;
height: 60px;
animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
div {
animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
#include ballb(-400px);
}
So I ended up going more of an old-school way. I took out the <transition /> wrapper around the <v-badge />, then added a watch function as such:
watch: {
cartItems: function(newValue, oldValue) {
const badge = document.querySelector('.v-badge__badge');
if (newValue !== oldValue) {
badge.classList.add('bounce');
this.delay(500).then(() => {
badge.classList.remove('bounce');
});
}
}
},
and my scss looks like:
#mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
#keyframes bouncein {
0%, 50% { #include ballb(-3px); }
25%, 75%, 100% { #include ballb() }
}
.bounce {
animation: bouncein 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
This gives the little "shake" of the badge I was looking for, however if someone has a more native Vue way of handling it, I'd absolutely love to see it.
In Vue 2.6.11, here is how I got a bit of a bounce, with some help from this answer from #Bill Criswell.
From his answer I got that I needed the :key on the badge element to force a re-render. The rest is straight out of Vue.js example here.
<transition name="bounce">
<v-badge
:key="item.comments.length"
v-if="item.comments && item.comments.length > 0"
>
<span slot="badge">
{{ item.comments.length }}
</span>
<v-icon>mdi-comment </v-icon>
</v-badge>
</transition>
And the associated SCSS just copied from the Vue example above:
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
#keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
Situation
I have a table with devices and their statuses. When I click on a specific button the rows that have the offline status need to have a highlight for a couple of seconds and then return back to normal.
What I have so far
<tr id="deviceRow" class="user-item" *ngFor="let device of group.devices" (click)="$event.stopPropagation()" [class.highlightOn]="this.offlineHighlight == true && device.onlineState == 'Offline'">
When I click on the button the offlineHighlight boolean becomes true and it adds the highlightOn class which is this.
.highlightOn {
background-color: rgb(255, 68, 65);
-webkit-animation: fade-out 3s ease-out both;
animation: fade-out 3s ease-out both;
}
#-webkit-keyframes fade-out {
0% {
background-color: rgba(255,51,47,1);
}
100% {
background-color: transparent;
}
}
#keyframes fade-out {
0% {
background-color: rgba(255,51,47,1);
}
100% {
background-color: transparent;
}
}
This adds the 'highlight' animation.
After the animation is completed I set the offlineHighlught boolean to false again in the button code.
showOfflineDevices() {
this.offlineHighlight = true;
this.tabIndex = 1;
setTimeout(function(){
this.offlineHighlight = false;
}, 3000);
}
It all works fine until the animation has completed. Standard the table rows have different background colors for each odd even row. When the animation is complete all the rows that had the highlightOn class have a white background color as you can see here.
TL:DR The background color of the table rows need to go back to normal after the animation is completed. The even rows are also white now, which need to be grey.
It's because you set background-color to transparent on fade-out, you can simply use transitions like this (just add and remove class with additional styles, don't override existing styles on fade-out):
setInterval(() => {
$(".color").addClass("selected");
setTimeout(() => {
$(".color").removeClass("selected")
}, 2500);
}, 5000);
div {
transition: background-color .5s ease;
}
div:nth-child(odd) {
background-color: lightgray;
}
div:nth-child(even) {
background-color: gray;
}
.selected {
background-color: green !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>1</div>
<div class="color">2</div>
<div class="color">3</div>
<div>4</div>
<div>5</div>
my React code
import React from 'react'
import ReactDOM from 'react-dom'
import { CSSTransition } from 'react-transition-group'
import './styles.css';
const Fade = ({ children, ...props }) => (
<CSSTransition
{...props}
timeout={600}
classNames="slide-filter"
>
{children}
</CSSTransition>
);
class FadeInAndOut extends React.Component {
state = {
show: false
}
render() {
return (
<div>
<button onClick={() => this.setState((prevState) => {
return { show: !prevState.show }
})}>Toggle</button>
<hr/>
<Fade in={this.state.show}>
<div className='greeting'>Hello world</div>
</Fade>
</div>
)
}
}
My CSS classes
.slide-filter-enter {
transform: translateX(50%);
opacity: 0;
transition: all 0.6s ease-in;
color:blue;
}
.slide-filter-enter-active {
transform: translateX(0);
color:red;
opacity: 1;
}
.slide-filter-exit {
transform: translateY(0);
opacity: 1;
color:red;
}
.slide-filter-exit-active {
color:blue;
transform: translateX(50%);
transition: all 0.6s ease-in;
}
Ok I can't understand why this is not working. It just resets to its original state after animating.
Also why is the "enter" or "enter-active" class not applied the first time it is rendered on the screen?
So you want the position of the "Fade" component to stay at the end position of the animation? Forgive me if this is not the behavior you are looking for but try removing the timeout={1000} prop on line 10.
I use this inline transform to iterate dynamically over slides
<div className="slides" style={{transform: `translate(${currentPosition}%, 0px)`, left: 0}}> {slider} </div>
</div>
which is working fine. However I would like to add some fadein to this element on position changing and the following css does not working
on element slides via css
opacity: 0;
animation: fadein 2s ease-in 1 forwards;
here is my fadein animation
#keyframes fadein {
from {
opacity:0;
}
to {
opacity:1;
}
}
What I'm missing in this case?
You can try something like this. That will give you idea how can you apply animations.
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
currentPosition: 0,
opacity: 0
}
}
componentDidMount() {
setTimeout(() => this.setState({
opacity: 1
}), 500);
}
handleClick() {
let self = this;
this.setState({
opacity: 0
}, () => setTimeout(() => self.setState({
opacity: 1
}), 2000))
}
render() {
return ( <
div >
<
div className = "slides"
style = {
{
transform: `translate(${this.state.currentPosition}%, 0px)`,
left: 0,
opacity: this.state.opacity
}
} > Some title to test animation < /div> <
button onClick = {
this.handleClick.bind(this)
} > Click < /button> <
/div>
)
}
}
React.render( < Test / > , document.getElementById('container'));
.slides {
transition: all 2s ease-in;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react-dom.min.js"></script>
<div id="container"></div>
Here is the fiddle.
Hope it helps.