Jagadeesh J
Jagadeesh J

Reputation: 1301

How to dynamically create '@-Keyframe' CSS animations?

I have a requirement to rotate a div and stop at a particular position ( The value will be received from the server).

I tried native JS to rotate and stop but it is eating up my CPU big time.

I can rotate with CSS animation but I need to create a class which will dynamically describe where to stop the animation. Something like

@-webkit-keyframes spinIt {
    100% {
        -webkit-transform: rotate(A_DYNAMIC_VALUE);
    }
}
@-moz-keyframes spinIt {
    100% {
        -webkit-transform: rotate(A_DYNAMIC_VALUE);
    }
}

Here is one reference

http://jsfiddle.net/bVkwH/8/

Upvotes: 82

Views: 109271

Answers (13)

mcpiroman
mcpiroman

Reputation: 355

This is now easily achievable with the new Web Animations API, which looks like this:

const anim = document.getElementById("foo").animate(
  [
    { transform: `rotate(${A_DYNAMIC_VALUE})` }
  ],
  { duration: 3000, iterations: Infinity }
);

// and later
anim.pause();

The first argument to .animate takes a list of keyframes, and the second takes the animation options (e.g. duration, how many times it repeats, etc).

Upvotes: 14

user7892745
user7892745

Reputation:

Alex Grande's answer works GREAT for a few keyframes. But, say you want to dynamically keep adding in keyframes over and over again, then your webpage get really laggy really quick. To solve this problem, just STOP creating new DOM elements. Rather, create 1 new DOM stylesheet, and just reuse it with the insertRule. If you want even more keyframes (like if you're generating a new keyframe every animationframe), then you need to set up a system which deletes old keyframes after they're no longer used. This is a good start to how something like this can be achieved.

var myReuseableStylesheet = document.createElement('style'),
    addKeyFrames = null;
document.head.appendChild( myReuseableStylesheet );
if (CSS && CSS.supports && CSS.supports('animation: name')){
    // we can safely assume that the browser supports unprefixed version.
    addKeyFrames = function(name, frames){
        var pos = myReuseableStylesheet.length;
        myReuseableStylesheet.insertRule(
            "@keyframes " + name + "{" + frames + "}", pos);
    }
} else {
    addKeyFrames = function(name, frames){
        // Ugly and terrible, but users with this terrible of a browser
        // *cough* IE *cough* don't deserve a fast site
        var str = name + "{" + frames + "}",
            pos = myReuseableStylesheet.length;
        myReuseableStylesheet.insertRule("@-webkit-keyframes " + str, pos);
        myReuseableStylesheet.insertRule("@keyframes " + str, pos+1);
    }
}

Example usage:

addKeyFrames(
    'fadeAnimation',
    '0%{opacity:0}' + 
    '100%{opacity:1}'
);

Also, Alex Grande, I am pretty sure that document.getElementsByTagName('head')[0] and type = 'text/css' hasn't been needed since IE8, and @keyframes aren't supported till IE10. Just saying...

Upvotes: 11

27px
27px

Reputation: 477

Found a simple idea with JavaScript by using CSS data URI.

Solution

function addNewCSS(css_text) {
  css_text = encodeURIComponent(css_text);
  const url = `data:text/css,${css_text}`;
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = url;
  document.head.appendChild(link);
}

Function accepts CSS code as text and adds it as a style.

Working

Converts the CSS text to URI encoded form (for passing as data URL). Then creates a link tag with href as the url and relation as "stylesheet" (here rel attribute is required and won't work if not added) Finally appends the link tag to head tag.

Example

function addNewCSS(css_text) {
  css_text = encodeURIComponent(css_text);
  const url = `data:text/css,${css_text}`;
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = url;
  document.head.appendChild(link);
}

const duration = 1;
const colour = ["#2196F3", "#E91E63"];
const css_data = `
  @keyframes change{
    0% {
      background: ${colour[0]};
    }
    100% {
      background: ${colour[1]};
    }
  }
  body {
    animation: change ${duration}s linear infinite alternate;
  }
`;

addNewCSS(css_data);
<html>
  <head></head>
  <body>
    <h1>Wait to see JS adding background color animation</h1>
  </body>
</html>

Conclusion

I haven't tested on all browsers, but works in chrome, and as it is added to the end of head tag it get priority from other tags in head, If you are planning to change values frequently, instead of adding new tags, try to edit the href of previously added tags.

Upvotes: 0

dale landry
dale landry

Reputation: 8600

With CSS variables: You can use the pseudo :root of the element to declare a css variable within the css rules, then manipulate that variable using Javascript.

:root {--variable-name:property;} which is basically the root element of the document <html>. Then change the value of the CSS root variable/s using JS with:

element.style.setProperty('--variable-name','value'). Pass the declared root variable --variable-name as the name and assign the new value. Then in your @keyframes css rules, add the root variable name, like: from: { top: var(--top-position)}, to the property within the offset @keyframe rule. Example:

:root {
  --top-position-start: 0px;
  --left-position-start: 0px;
  --top-position-end: 200px;
  --left-position-end: 200px;
}

.element {
  top: var(--top-position-start);
  left: var(--left-position-start);
  animation: movePos 1s ease-in;
}

@keyframes movePos {
  from: {
    top: var(--top-position-start);
    left: var(--left-position-start);
  } 
  to: {
    top: var(--top-position-end);
    left: var(--left-position-end);
  }
}

Then the JS would like something like:

let ran = getRandomInt(99);
let skew = ran + getRandomInt(10);
root.style.setProperty('--top-position-end', `${ran}vw`);
root.style.setProperty('--left-position-end', `${skew}vw`);

By using the CSS variable on the root element, you are able to pass it along to the @keyframes event.

See the following working example using randomly placed div using CSS left and background-color:rgb()(red, green, blue) passed using the html:root style to @keyframes within CSS.

let root = document.documentElement;
let rain = document.querySelectorAll('.drop');

function getMaxInt(max) {
  return Math.floor(Math.random() * Math.floor(max));
}

function getMinMaxInt(min, max) {
  return Math.random() * (max - min) + min;
}
// set an interval to drop the div from randomly positioned view widths on the screen
setInterval(() => {
  let ran = getMaxInt(86);
  let skew = ran + getMaxInt(10);
  let circle = `${getMinMaxInt(3,15)}px`;
  root.style.setProperty('--keyframeLeftStart', `${ran}vw`);
  root.style.setProperty('--keyframeLeftEnd', `${skew}vw`);  
  root.style.setProperty('--animationDuration', `${ getMaxInt(2500)}ms`); 
  root.style.setProperty('--width', circle);
  root.style.setProperty('--height', circle);
  root.style.setProperty('--red', getMinMaxInt(100, 255));
  root.style.setProperty('--green', getMinMaxInt(100, 255));
  root.style.setProperty('--blue', getMinMaxInt(100, 255));
}, getMaxInt(3500))
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

/* here we define some css variables for the document :root 
   essentially, these will be the first iteration of the elements style
   then JS will take voer and set the values from script */
:root {
  --keyframeTop: 0;
  --keyframeBottom: 98vh;
  --keyframeLeftStart: 2vw;
  --keyframeLeftEnd: 10vw;
  --animationDuration: 1s;
  --width: 5px;
  --height: 5px;
  --red: 100;
  --green: 100;
  --blue: 100;
}

body {
  width: 100vw;
  height: 100vh;
  background-color: #000;
}

#main {
  width: calc(100vw - var(--width));
  height: calc(100vh - var(--height));
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
}

.drop {
  width: var(--width);
  height: var(--height);
  border-radius: 50%;
  position: absolute;
  animation: dropping var(--animationDuration) ease-in infinite;
  top: var(--keyframeTop);
  left: var(--keyframeLeftStart);
  background-color: rgb(var(--red),var(--green), var(--blue));
}

@keyframes dropping {
  0% {
    top: var(--keyframeTop);
    left: var(--keyframeLeftStart);
    background-color: rgb(var(--red),var(--green), var(--blue));
  }
  50% {
    background-color: rgb(var(--green),var(--blue), var(--red));
  }
  100% {
    top: var(--keyframeBottom);
    left: var(--keyframeLeftEnd);
    background-color: rgb(var(--blue),var(--red), var(--green));
  }
}
<div id="main">
    <div class="drop"></div>
</div>

Upvotes: 2

NVRM
NVRM

Reputation: 13057

Setting a @keyframe in one call with JavaScript, and use it, using append(), Object.assign(), and template strings.

document.body.append(
  Object.assign(document.createElement("style"), {
    textContent: `@keyframes coolrotate { from { transform: scale(1, 1) translate(-0.1em, 0)} to { transform: scale(-1, 1) translate(0, 0) }} small { display: inline-block; font-size:2.3em; animation: 1s infinite alternate coolrotate } body {font-size: x-large}`
  }),
  Object.assign(document.createElement("span"), {
    innerHTML: `<span>c</span><small>o</small><span>o</span><small>L</small><small>...</small>`,
    style: "font-weight: 1000; font-size: 3.3em;"
  })  
)

Upvotes: 1

sandrina-p
sandrina-p

Reputation: 4160

Let me share an updated (2019) answer to this.

Yes, it's possible without Javascript using CSS Variables (supported by all modern browsers).

--lightScaleStart: 0.8;

.light {
    animation: grow 2s alternate infinite ease-in-out;
}

.light.yellow {
    --lightScaleEnd: 1.1;
}

.light.red {
    --lightScaleEnd: 1.2;
}

@keyframes grow {
  from {
    transform: scale(var(--lightScaleStart));
  }
  to {
    transform: scale(var(--lightScaleEnd));
  }
}

See demo on Codepen Dynamic CSS Animations with CSS Variables

Edit: Here's a CSS Tricks article about it too.

Upvotes: 15

Deep Sharma
Deep Sharma

Reputation: 3473

well i don't think it is easy to create dynamic @keyframes they are inflexible because they must be hard-coded.

Transitions are a little easier to work with, as they can gracefully respond to any CSS changes performed by JavaScript.

However, the complexity that CSS transitions can give you is pretty limited — an animation with multiple steps is difficult to achieve.

This is a problem that CSS @keyframe animations are meant to solve, but they don’t offer the level of dynamic responsiveness that transitions do.

but these links might help you

Link1 : a tool that generates a @-webkit-keyframe animation with many tiny steps. This opens the door to an unlimited selection of easing formula.

Link2 it will be a great help for you to take it as a base as it provides a UI to create animations and exports it to CSS code.

I guess this solution will definitely work for you. Its is used for dynamic keyframes

Upvotes: 49

Matthias Southwick
Matthias Southwick

Reputation: 121

You could create a new stylesheet with the animation you want in it. For Example:

function addAnimation(keyframe){
     var ss=document.createElement('style');
     ss.innerText=keyframe;
     document.head.appendChild(ss);
}

This would create a new stylesheet with your animation.
This method has only been tested in Chrome.

Upvotes: 1

Danziger
Danziger

Reputation: 21161

You can create a <style> element, set its content to the CSS you want, in this case, the declaration of your animation and add it to the <head> of the page.

Also, as others have suggested, if you need to create many different animations, then it would be better to reuse a single <style> tag rather than creating multiple of them and add the new styles using CSSStyleSheet.insertRule().

Lastly, if you can use ES6's template literals/strings, your code will look much cleaner:

let dynamicStyles = null;

function addAnimation(body) {
  if (!dynamicStyles) {
    dynamicStyles = document.createElement('style');
    dynamicStyles.type = 'text/css';
    document.head.appendChild(dynamicStyles);
  }
  
  dynamicStyles.sheet.insertRule(body, dynamicStyles.length);
}

addAnimation(`
  @keyframes myAnimation { 
    0% { transform: rotate(0); }
    20% { transform: rotate(${ 360 * Math.random() }deg); }
    60% { transform: rotate(${ -360 * Math.random() }deg); }
    90% { transform: rotate(${ 360 * Math.random() }deg); }
    100% { transform: rotate(${ 0 }deg); }
  }
`);

document.getElementById("circle").style.animation = 'myAnimation 3s infinite';
html,
body {
  height: 100vh;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0;
}

#circle {
  width: 100px;
  height: 100px;
  box-shadow:
    0 0 48px -4px rgba(0, 0, 0, .25),
    0 0 0 4px rgba(0, 0, 0, .02);
  border-radius: 100%;
  position: relative;
  overflow: hidden;
}

#circle::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-2px);
  border-left: 4px solid #FFF;
  height: 24px;
  box-shadow: 0 -4px 12px rgba(0, 0, 0, .25);
}
<div id="circle"></div>

Or even better:

let dynamicStyles = null;

function addAnimation(name, body) {
  if (!dynamicStyles) {
    dynamicStyles = document.createElement('style');
    dynamicStyles.type = 'text/css';
    document.head.appendChild(dynamicStyles);
  }
  
  dynamicStyles.sheet.insertRule(`@keyframes ${ name } {
    ${ body }
  }`, dynamicStyles.length);
}

addAnimation('myAnimation', `
  0% { transform: rotate(0); }
  20% { transform: rotate(${ 360 * Math.random() }deg); }
  60% { transform: rotate(${ -360 * Math.random() }deg); }
  90% { transform: rotate(${ 360 * Math.random() }deg); }
  100% { transform: rotate(${ 0 }deg); }
`);

document.getElementById("circle").style.animation = 'myAnimation 3s infinite';
html,
body {
  height: 100vh;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0;
}

#circle {
  width: 100px;
  height: 100px;
  box-shadow:
    0 0 48px -4px rgba(0, 0, 0, .25),
    0 0 0 4px rgba(0, 0, 0, .02);
  border-radius: 100%;
  position: relative;
  overflow: hidden;
}

#circle::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-2px);
  border-left: 4px solid #FFF;
  height: 24px;
  box-shadow: 0 -4px 12px rgba(0, 0, 0, .25);
}
<div id="circle"></div>

Upvotes: 0

Martin Wantke
Martin Wantke

Reputation: 4571

In JavaScript is it possible to access to the style sheet with document.styleSheets. Every sheet has a rule and/or cssRule list (browser depending) and a CSSStyleSheet.insertRule() method.

This method allows you to add a new keyframe raw as a string:

JavaScript

function insertStyleSheetRule(ruleText)
{
    let sheets = document.styleSheets;

    if(sheets.length == 0)
    {
        let style = document.createElement('style');
        style.appendChild(document.createTextNode(""));
        document.head.appendChild(style);
    }

    let sheet = sheets[sheets.length - 1];
    sheet.insertRule(ruleText, sheet.rules ? sheet.rules.length : sheet.cssRules.length);
}

document.addEventListener("DOMContentLoaded", event =>
{
    insertStyleSheetRule("@keyframes spinIt { 0% { transform: rotate(-20deg); } 100% { transform: rotate(20deg); } }");

    insertStyleSheetRule("#box { " + 
        "animation: spinIt 1s infinite alternate cubic-bezier(0.5,0,0.5,1); " + 
        "width: 64px; height: 64px; background-color: red; border: 4px solid black; " + 
    "}");
});

html

<div id="box"></div>

demo: https://jsfiddle.net/axd7nteu/

Upvotes: 1

WheatBerry
WheatBerry

Reputation: 31

You can change the style in CSSKeyframeRule, and this works fine for me in Chrome, just as the code below. Hope this will help:)

<html>

<head>
	<style>
		#text {
			display: inline-block;
		}
	</style>
</head>

<body>
	<div id="text">TEXT</div>
	<script>
	
		// Dynamically create a keyframe animation
		document.styleSheets[0].insertRule('\
			@keyframes anim {\
				from { transform: rotateZ(0deg);   }\
				to   { transform: rotateZ(360deg); }\
			}'
		);
		var div = document.getElementById('text');
		div.style.animation = 'anim 1s linear forwards';
		
		// This function will change the anim
		function stopAtSomeDeg(d) {
			var ss = document.styleSheets[0];
			var anim;
			for (var i in ss.cssRules) {
				// Find your animation by name
				if (ss.cssRules[i].name === 'anim') {
					anim = ss.cssRules[i];
					break;
				}
			}
			var stopFrame = anim.cssRules[1]; // This indicates the second line of "anim" above.
			
			// Change any attributes
			stopFrame.style.transform = 'rotateZ(' + d + 'deg)';
		}
		
		stopAtSomeDeg(180);
	</script>
</body>
</html>

Upvotes: 3

cippu_lux
cippu_lux

Reputation: 1

the user7892745 wont work for me, need some little adjustement

1° "pos" not understand wot should be, but the console log say "undefined" so I've remove " , pos"

2° " myReuseableStylesheet.insertRule" give me error " is not a function" so I used "innerHTML" insted of "insertRule"

3° finally I've moved " document.head.appendChild( myReuseableStylesheet );" at the end

but after this it work fine and it's exact what I looking for. thanks a lot user7892745 :D

maybe the problem I had, come form the way I use it

this is the script i used with it

var getclass = document.getElementsByClassName("cls");
var countclass = getclass.length;
for (var i=0; i <countclass; i++ ){
    getclass[i].addEventListener('mouseover', function(){
        // get the data-name value to show element whose id are the same
        var x=  this.getAttribute("data-name"); 
        var y =document.getElementById(x);
            y.style.display="block";
            // because the element to show have fixed width, but different text length, they have different height
            // so I need to get the highness, then use the value of height to define the 100% value of animation
            // or the longer ones will be cutted an the shorten have a lot of empty space a the end
        var yHeig= Math.round(parseInt(getComputedStyle(y).getPropertyValue('height')));
            yHeig_ = yHeig - 10; // to shorten a bit the time from end and new passage
        console.log(yHeig+" - "+ yHeig_);
        addKeyFrames(
                'showMe',
                '0%{top:35px;}' + 
                '100%{top:-'+ yHeig_ +'px;}'
            );
        y.style.animation="showMe 7s linear infinite";

    },false);

    getclass[i].addEventListener('mouseout', function(){
        var x=  this.getAttribute("data-name");
        document.getElementById(x).style.display="none";
    },false);
}

i know thath a html marquee cuold seem symple to do the same thing, but dont work well,

Upvotes: 0

Alex Grande
Alex Grande

Reputation: 8027

You can insert stylesheet rules dynamically to override previous styles in the head. This helps avoid adding yet another library for a single task.

var style = document.createElement('style');
style.type = 'text/css';
var keyFrames = '\
@-webkit-keyframes spinIt {\
    100% {\
        -webkit-transform: rotate(A_DYNAMIC_VALUE);\
    }\
}\
@-moz-keyframes spinIt {\
    100% {\
        -webkit-transform: rotate(A_DYNAMIC_VALUE);\
    }\
}';
style.innerHTML = keyFrames.replace(/A_DYNAMIC_VALUE/g, "180deg");
document.getElementsByTagName('head')[0].appendChild(style);

Upvotes: 73

Related Questions