I have a Create-React-app project that requires me to have some information constantly scroll across the screen. My current implementation:
import React from 'react'
import './styles/animation.css'
const Tickerbar = (props) => {
const displayNameStyle = {
color:'white',
}
const midStyle = {
color:'#CDBFBF',
marginRight:'20px'
}
const displayNames = Object.keys(props.tickers)
const displayNameAndMid = displayNames.map(cusip => {
return(
<span key={cusip}>
<span style={displayNameStyle}>
<b>{cusip.split('/').join('.')}: </b>
</span>
<span style={midStyle}>
<b>{(props.tickers[cusip]).toFixed(2)}</b>
</span>
</span>
)
})
return(
<div className="marquee">
<p>
{displayNameAndMid}
</p>
</div>
)
}
export default Tickerbar;
.marquee {
width: 100vw;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
height: 51px;
position: absolute;
bottom: 10px;
}
.marquee p {
display: inline-block;
padding-left: 100%;
animation: marquee 100s linear 1s infinite;
-webkit-animation: marquee 100s linear 1s infinite;
color:white;
}
#keyframes marquee {
0% { transform: translate(-50%, 0); }
100% { transform: translate(-100%, 0); }
}
#-webkit-keyframes marquee {
0% { -webkit-transform: translate(-50%, 0); }
100% { -webkit-transform: translate(-100%, 0); }
The problem with this however is that the text scrolls across, then when it ends, I can see it start up again. Ideally, as the text is ending on the left side of the screen, it will start scrolling in again on the right side of the screen. I've seen some implementations that work by writing the text twice, and having the second set of text start scrolling halfway through the animation time, but when I try this the letters end up on top of each other and are unreadable.
Related
I have React code with a CSS animation in a codesandbox and on my staging site.
You will notice that over time, the animation timing drifts. After a certain number of loops it presents the text too early and is not in sync with the animation.
I have tried changing the timing making the array switch happen faster and slower.
Any ideas would be greatly appreciated.
import "./styles.css";
import styled, { keyframes } from "styled-components";
import React, { useEffect, useState } from "react";
const animation = keyframes`
0% { opacity: 0; transform: translateY(-100px) skewX(10deg) skewY(10deg) rotateZ(30deg); filter: blur(10px); }
25% { opacity: 1; transform: translateY(0px) skewX(0deg) skewY(0deg) rotateZ(0deg); filter: blur(0px); }
75% { opacity: 1; transform: translateY(0px) skewX(0deg) skewY(0deg) rotateZ(0deg); filter: blur(1px); }
100% { opacity: 0; transform: translateY(-100px) skewX(10deg) skewY(10deg) rotateZ(30deg); filter: blur(10px); }
`;
const StaticText = styled.div`
position: absolute;
top: 100px;
h1 {
color: #bcbcbc;
}
span {
color: red;
}
h1,
span {
font-size: 5rem;
#media (max-width: 720px) {
font-size: 3rem;
}
}
width: 50%;
text-align: center;
left: 50%;
margin-left: -25%;
`;
const Animate = styled.span`
display: inline-block;
span {
opacity: 0;
display: inline-block;
animation-name: ${animation};
animation-duration: 3s;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
animation-fill-mode: forwards;
animation-iteration-count: infinite;
font-weight: bold;
}
span:nth-child(1) {
animation-delay: 0.1s;
}
span:nth-child(2) {
animation-delay: 0.2s;
}
span:nth-child(3) {
animation-delay: 0.3s;
}
span:nth-child(4) {
animation-delay: 0.4s;
}
span:nth-child(5) {
animation-delay: 0.5s;
}
`;
export default function App() {
const array = ["wood", "cork", "leather", "vinyl", "carpet"];
const [text, setText] = useState(array[0].split(""));
const [countUp, setCountUp] = useState(0);
useEffect(() => {
const id = setTimeout(() => {
if (countUp === array.length -1) {
setCountUp(0);
} else {
setCountUp((prev) => prev + 1);
}
}, 3000);
return () => {
clearTimeout(id);
};
}, [countUp]);
useEffect(() => {
setText(array[countUp].split(""));
}, [countUp]);
return (
<div className="App">
<StaticText>
<h1>More than just</h1>
<Animate>
{text.map((item, index) => (
<span key={index}>{item}</span>
))}
</Animate>
</StaticText>
</div>
);
}
There are multiple potential issues here. For one, the animation runs for up to 3.5 seconds (due to the delay) but the text changes every 3 seconds, so the text change would trigger before the last character finishes animating.
Even if the text and animation were both set to 3s, the problem is that CSS animation and setTimeout/setInterval timing aren't perfect. These should be considered rough estimates. A setTimeout can take 3s to fire, or 3.1s, and even if it fires on time, React has to do work before another one is set. Drift can and will occur, so the animation should run in an event-driven manner whenever the text changes, not as an infinite loop that we assume will stay in sync with React and the timeout.
Adjustments you can try to fix these issues with include:
Remove the animation-iteration-count: infinite; property. This holds us accountable for triggering the animation in response to re-renders, not in a separate, likely-out-of-sync loop.
Change the setTimeout timeout to 3500, or something that is at least as large as the longest animation duration to make sure the animation isn't chopped off partway through.
Provide random keys to your letter <span>s to force rerenders as described in How to trigger a CSS animation on EVERY TIME a react component re-renders. To be precise, that could be <span key={Math.random()}>{item}</span>.
You can have key clashes using Math.random(), so using an incrementing state counter or integrating Date.now() for keys is a more robust way to go here.
I'm trying to animate a hamburger menu by having the bottom and top line translate to the middle and then rotate into an X and want to reverse the animation when the X is clicked. Using jquery I'm toggling the class menu-open and menu-closed. When I remove the CSS for the menu-closed animation, it fires just fine but when I add the CSS back the animations just skip to the last frame. It forms what I want but just refuses to use the animation fully.
CSS
.navbar .mobile-menu.menu-open .line::before {
animation: menu-open-top 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line {
animation: menu-middle 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line::after {
animation: menu-open-bottom 250ms linear forwards;
}
.navbar .mobile-menu.menu-closed .line::before {
animation: menu-open-top 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line {
animation: menu-middle 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line::after {
animation: menu-open-bottom 250ms linear reverse;
}
Animation
#keyframes menu-open-top {
30% {
bottom: 0;
}
60% {
bottom: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(45deg) translate(5px, 5px);
visibility: visible;
}
}
#keyframes menu-middle {
40% {
visibility: hidden;
}
to {
visibility: hidden;
}
}
#keyframes menu-open-bottom {
30% {
top: 0;
}
60% {
top: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(-45deg) translate(6px, -6px);
visibility: visible;
}
}
JS
$(".mobile-menu").click(expandMenu);
function expandMenu() {
$(".primary-nav").toggleClass("menu-expand");
$(this).toggleClass("menu-open menu-closed");
}
I must be missing something or maybe I need to add new keyframes for the reverse animation but that feels like it would be unnecessary.
edit: here is the html as well
HTML
<div class="mobile-menu menu-closed">
<div class="line"></div>
</div>
Here's how to do it using simple prop value changes with careful timing. I guess it can be done using #keyframe animations as well, but I find them more difficult to follow/control/sync, at least in this case, considering it's (basically) a two-step animation.
document.querySelector('.mobile-menu').addEventListener('click', ({
target
}) => {
target.closest('.mobile-menu').classList.toggle('menu-open');
})
.mobile-menu {
--duration: 0.42s;
--size: 3rem;
--padding: 0.5rem;
--color: red;
--distance-timing: cubic-bezier(0.5, 0, 0.3, 1);
--rotation-timing: cubic-bezier(0.4, 0, 0.2, 1);
width: var(--size);
height: var(--size);
padding: var(--padding);
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
.mobile-menu * {
box-sizing: border-box;
}
.mobile-menu>div {
border: 1px solid var(--color);
height: 0;
width: 100%;
position: relative;
transform-origin: 50% 50%;
transition:
top calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
bottom calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
transform calc(0.8 * var(--duration)) var(--rotation-timing) 0s;
}
.mobile-menu> :nth-child(1) {
top: calc(var(--padding)/2);
}
.mobile-menu> :nth-child(3) {
bottom: calc(var(--padding)/2);
}
.mobile-menu.menu-open>div {
transition:
top calc(0.4 * var(--duration)) var(--distance-timing) 0s,
bottom calc(0.4 * var(--duration)) var(--distance-timing) 0s,
transform calc(0.8 * var(--duration)) var(--rotation-timing) calc(0.2 * var(--duration));
}
.mobile-menu.menu-open> :nth-child(1) {
top: calc(50% - 1px);
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(2) {
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(3) {
bottom: calc(50% - 1px);
transform: rotate(-0.125turn);
}
<div class="mobile-menu">
<div></div>
<div></div>
<div></div>
</div>
Same thing, in SCSS: https://jsfiddle.net/websiter/dybre2f9/
I've extracted the values into CSS vars, so it can be reused and modified with ease. Feel free to tweak it to your liking.
Note: the reason why I'm using bottom and top to animate the movement (and not translateY - which is slightly more performant) is because I wanted the two animations to be completely independent of each other, to allow me to play with various overlapping values and timing functions. What I've come up with doesn't respect the requirement 100% (as in, it slightly overlaps the rotation with the movement - but I'm doing it on purpose, as not overlapping them looks too mechanical). When overlapped, the entire animation seems more organic. It's like the button is alive and happy to have been asked to do the animation. Or maybe I'm just a bit crazy, I don't know...
so I have a simple side bar that I want to slide in and out using css animation, now the slide animation in is working the problem that I am facing is making the animation for the slideout to work.
Can I please get help on that..
Html(Sidebar)
<div class="SideBarMenu" id="SideBarMenu">
<div class="sidebar-menu-header">
<h2 class="nav-bleft-companyname">
Sofast<span class="nav-bleft-periodmark">Cart.</span>
</h2>
</div>
<hr />
<h4 class="sidebar-menuLink">MenuItem1</h4>
<h4 class="sidebar-menuLink">MenuItem2</h4>
<h4 class="sidebar-menuLink">MenuItem3</h4>
<h4 class="sidebar-menuLink">MenuItem4</h4>
<h4 class="sidebar-menuLink">MenuItem5</h4>
</div>
JS Function triggered to toggle menu
const OpenMenu = () => {
const menu = document.getElementById("SideBarMenu");
if (menu.style.display === "block") {
menu.classList.add("sidebar-closed");
menu.style.animation = "slideOut 0.4s backwards";
menu.style.display = "none";
} else {
menu.style.display = "block";
menu.style.animation = "slideIn 0.4s forwards";
menu.classList.remove("sidebar-closed");
}
};
Side Bar css
.SideBarMenu {
top: 0% !important;
z-index: 999;
position: fixed;
background-color: #333333;
height: 100vh;
color: white;
width: 20%;
transform: translateX(-350px);
padding: 2rem;
}
.sidebar-menuLink {
margin-top: 1rem;
}
#keyframes slideIn {
100% {
transform: translateX(0px);
}
}
#keyframes slideOut {
100% {
transform: translateX(-350px);
}
}
You can use css transition to achieve that. I also advise you to put all your styles in css classes, you don't need to apply the styles in javascript.
const animate = () => {
const elem = document.getElementById("my-elem");
if (!elem.classList.contains('elem-out')) {
elem.classList.add("elem-out");
} else {
elem.classList.remove("elem-out");
}
};
const btn = document.getElementById("my-btn");
btn.addEventListener("click", animate)
.elem {
width: 300px;
height: 50px;
background-color: red;
margin-bottom: 10px;
}
.elem:not(.elem-out) {
transition: transform 0.4s ease-in;
transform: translateX(0px);
}
.elem-out {
transition: transform 0.4s ease-out;
transform: translateX(-350px);
}
<div id="my-elem" class="elem"></div>
<button id="my-btn">toggle animate</button>
Do not change the display property, the transition will brake. There's no transition for propperty like display, if you want a fadeIn / fadeOut effect too, you can put a transition on opacity property
What is the best way to have an object scale and then perform a bounce animation at that scale factor before going back to the original scale factor. I realize I could do something like scaling it to 2.2, then 1.8, then 2.0, but I'm looking for a way where you just have to perform the bounce animation on the scale factor because my scale factor will change.
Here is my example. Basically I want to combine the two to work like I said but as you can see the bounce animation performs based off the image size prior to scaling.
I need all sides of the image to bounce equally, like the example.
function myFunction() {
var image = document.getElementById('test');
image.style.WebkitTransform = ('scale(2,2)');
image.classList.remove('bounce');
image.offsetWidth = image.offsetWidth;
image.classList.add('bounce') ;
}
div#test {
position:relative;
display: block;
width: 50px;
height: 50px;
background-color: blue;
margin: 50px auto;
transition-duration: 1s;
}
.bounce {
animation: bounce 450ms;
animation-timing-function: linear;
}
#keyframes bounce{
25%{transform: scale(1.15);}
50%{transform: scale(0.9);}
75%{transform: scale(1.1);}
100%{transform: scale(1.0);}
}
<div id='test'> </div>
<button class = 'butt' onclick = 'myFunction()'>FIRST</button>
You can try the use of CSS variable in order to make the scale factor dynamic within the keyframe:
function myFunction(id,s) {
var image = document.querySelectorAll('.test')[id];
image.style.setProperty("--s", s);
image.classList.remove('bounce');
image.offsetWidth = image.offsetWidth;
image.classList.add('bounce');
}
div.test {
display: inline-block;
width: 50px;
height: 50px;
background-color: blue;
margin: 50px;
transform:scale(var(--s,1));
}
.bounce {
animation: bounce 450ms;
animation-timing-function: linear;
}
#keyframes bounce {
25% {
transform: scale(calc(var(--s,1) + 0.2));
}
50% {
transform: scale(calc(var(--s,1) - 0.1));
}
75% {
transform: scale(calc(var(--s,1) + 0.1));
}
100% {
transform: scale(var(--s,1));
}
}
<div class='test'> </div>
<div class='test'> </div>
<div class='test'> </div>
<div>
<button class='butt' onclick='myFunction(0,"2")'>first</button>
<button class='butt' onclick='myFunction(1,"3")'>Second</button>
<button class='butt' onclick='myFunction(2,"0.5")'>third</button>
<button class='butt' onclick='myFunction(1,"1")'>second again</button>
</div>
Dispatch each transform on a different wrapping element.
This way you can achieve several level of transforms, all relative to their parent wrapper.
Then you just need to set your animation to fire after a delay equal to the transition's duration:
btn.onclick = e => {
// in order to have two directions, we need to be a bit verbose in here...
const test = document.querySelector('.test');
const classList = test.classList;
const state = classList.contains('zoomed-in');
// first remove previous
classList.remove('zoomed-' + (state ? 'in' : 'out'));
// force reflow
test.offsetWidth;
// set new one
classList.add('zoomed-' + (state ? 'out' : 'in'));
};
div.test {
position: relative;
display: inline-block;
margin: 50px 50px;
}
div.test div{
width: 100%;
height: 100%;
transition: transform 1s;
}
div.test.zoomed-in .scaler {
transform: scale(2);
}
div.test.zoomed-in .bouncer,
div.test.zoomed-out .bouncer {
animation: bounce .25s 1s;/* transition's duration delay */
}
div.test .inner {
background-color: blue;
width: 50px;
height: 50px;
}
#keyframes bounce {
0% {
transform: scale(1.15);
}
33% {
transform: scale(0.9);
}
66% {
transform: scale(1.1);
}
100% {
transform: scale(1.0);
}
}
<div class="test">
<div class="scaler">
<div class="bouncer">
<div class="inner"></div>
</div>
</div>
</div>
<button id="btn">toggle zoom</button>
Take a look at this simple JsFiddle I created.
What it does is simply inserts a new li element with a slide effect from left, when the ul display is on flex and inline-block.
Something similar to that is Stackoverflow chat avatars when someone joins.
#-webkit-keyframes enter {
0% { /*opacity:0;*/ -webkit-transform: translateX(-100%); }
100% { /*opacity:1;*/ -webkit-transform: translateX(0px); }
}
#-webkit-keyframes moves {
0% { /*opacity:0;*/ -webkit-transform: translateX(-50px); }
100% { /*opacity:1;*/ -webkit-transform: translateX(0px); }
}
in my enter animation, I start with translate -100% because I want my item come from left distanced by his size.
and in the moves animation, I want to move the whole ul to the right, by the size of the entering element.
Now I set it hard-coded, to 50px because my elements are set to 50px:
li {
width: 50px;
height: 50px;
display: inline-block;
background-color: red;
}
How can I make it calculate the width OR height automatically, on how much to translate the ul?
Example: Calculate these 50px automatically
You can do this by animating only the added element by using negative margin:
setTimeout(() => {
var item = $("<li></li>").addClass("enter");
$("ul").prepend(item).addClass('move');
}, 2000);
ul {
display: flex;
transition-timing-function: ease-out;
transition: all .2s;
}
li {
--h:50px;
width: 50px;
height: var(--h);
display: inline-block;
background-color: red;
}
.enter {
animation: enter 1s;
}
#keyframes enter {
0% {
transform: translateX(-100%);
margin-left: calc(var(--h) * -1);
}
100% {
transform: translateX(0px);
margin-left: 0
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
<li class="my-flex-thing">a</li>
<li class="my-flex-thing">b</li>
</ul>