Can we restrict CSS keyframe animations to a scope - css

I wonder if it's possible to restrict keyframe animations to a scope based on classnames. The benefit would be to be able to use the same animation-name multiple times without getting issues. I couldn't find any infos about that..
In case it's not possible:
are there any best practices to handle naming conflicts?

I used to use something like SCSS to generate automatically created names for my keyframes. They might not be as descriptive, but they ensure uniqueness. Something like:
$animation-id-count: 0 !global;
#function animation-id {
$animation-id-count: $animation-id-count + 1;
#return animation-id-#{$animation-id-count};
}
After this, just use the function in your code like this:
.class {
$id: animation-id();
#keyframes #{$id}{
...keyframes
}
animation: $id 1s infinite;
}
That way if you insert it anywhere else in your SCSS or move it, it will still match the right animation, and it stops namespaces from overlapping in any way.

Here is a JSX approach (you will need object-hash for that).
The following example shows how to define different animations with respective unique ID based on transform: scale(n). For that purpose, define a function which returns the keyframes and its ID. The keyframes ID is a custom string suffixed with a hash of the function options (e.g. the scale factor).
(Be careful of CSS custom identifier, e.g. do not include a . in your ID. See MDN: < custom-ident >.)
import hash from "object-hash";
const keyFramesScale = (options = {}) => {
let { transforms, id, scale } = options;
transforms = transforms || "";
scale = scale || 1.25;
const keyFramesId = `scale${id ? "-" + id : ""}-${hash(options).substring(0, 6)}`;
const keyFrames = {
[`#keyframes ${keyFramesId}`]: {
"100%": {
transform: `scale(${scale}) ${transforms}`,
},
"0%": {
transform: `scale(1) ${transforms}`,
}
}
};
return [keyFramesId, keyFrames];
};
How to use it:
const [scaleUpId, keyFramesScaleUp] = keyFramesScale({ scale: 1.25, transforms: "rotate(-30deg)", id: "up" });
const [scaleDownId, keyFramesScaleDown] = keyFramesScale({ scale: 0.75, transforms: "rotate(-30deg)", id: "down" });
// scaleUpId = "scale-up-c61254"
// scaleDownId = "scale-down-6194d5"
// ...
<tag style={{
...keyFramesScaleUp,
...keyFramesScaleDown,
...(!hasTouchScreen && isActive && !isClicked && {
animation: `${scaleUpId} 0.5s infinite alternate linear`,
"&:hover": {
animation: "none",
},
}),
...(isClicked && {
animation: `${scaleDownId} .25s 1 linear`,
}),
}} />
Of course, you can write a more generic function that hashes the whole key frames and assign it an ID based on that.
EDIT
To concretize what has been said, here is the generic approach. We first define a generic function that takes an animation name (e.g. scale, pulse, etc.), its keyframes (which can be an object or a function), and optionally keyframes parameters and its default values.
import hash from "object-hash";
const createKeyFramesId = (id, keyFrames) => {
return `${id}-${hash(keyFrames).substring(0, 6)}`;
};
const genericKeyFrames = (name, keyFrames, defaults = {}, options = {}) => {
if (typeof keyFrames === "function") {
// The order of defaults & options is important: the latter overrides the former.
keyFrames = keyFrames({ ...defaults, ...options });
}
const keyFramesId = createKeyFramesId(name, keyFrames);
const keyFramesObject = {
[`#keyframes ${keyFramesId}`]: keyFrames
};
return [keyFramesId, keyFramesObject];
};
From now on, we can define all kind of animations. Their usage is the same as above.
export const keyFramesPulse = () =>
genericKeyFrames("pulse", {
"100%": {
opacity: "1",
},
"0%": {
opacity: "0.5",
},
});
export const keyFramesRotate = (options = {}) => {
const defaults = {
rotate: 360,
transforms: "",
};
const rotateKeyFrames = ({ rotate, transforms }) => {
return {
"100%": {
transform: `rotate(${rotate}deg) ${transforms}`,
}
}
};
return genericKeyFrames(`rotate`, rotateKeyFrames, defaults, options);
};
export const keyFramesScale = (options = {}) => {
const defaults = {
scale: 1.25,
transforms: ""
};
const scaleKeyFrames = ({ scale, transforms }) => {
return {
"100%": {
transform: `scale(${scale}) ${transforms}`,
},
"0%": {
transform: `scale(1) ${transforms}`,
}
}
};
return genericKeyFrames(`scale`, scaleKeyFrames, defaults, options);
};
What it looks like in DevTools:

Related

CSS: Set max-height to same value as height becomes when "none" max-height?

So I am making a dropdown that uses transition of max-height and opacity, for example if the dropdown was 500 pixels in height, I would use 500px for max-height when open, and 0 when closed. However, my dropdown's height is auto calculated. I need to set max-height to this auto calculated height, so that the transition has correct timin. Otherwise, if I used a larger max-height value, when being closed the dropdown would not move until max-height had transitioned below the true value, and then suddenly move faster for the remaining time. Any way to do this?
So I found a solution. I just set the maxHeight to the offsetHeight before triggering closing of my dropdown.
I also needed to use setTimeout of 0 milliseconds for triggering closing it though after running the below function.
export function retreatDropdown(e: HTMLElement) {
e.style.maxHeight = `${e.offsetHeight}px`;
}
I could do a similar trick for opening animation, but you won't know the correct maxHeight until it is opened, so my guess this is more tricky (but less important than closing animation IMO).
EDIT: Complete (TypeScript) solution for both closing/opening dropdown
const defaultTransitionTime = 200;
export function toggleDropdown(
e: HTMLElement,
value: boolean = null,
toggleAction: () => void = null // Function that toggles true/false for accordion open
) {
if (value) {
openDropdown(e, toggleAction);
} else {
retreatDropdown(e, toggleAction);
}
}
export function openDropdown(
e: HTMLElement,
openAction: () => void = null
) {
if (openAction) setTimeout(() => openAction(), 0);
e.style.display = "block";
setTimeout(() => {
const offsetHeight = e.offsetHeight;
e.style.maxHeight = "0px";
setTimeout(() => {
e.style.maxHeight = `${offsetHeight}px`;
}, 5);
}, 5);
setTimeout(() => {
e.style.maxHeight = "none";
}, defaultTransitionTime);
}
export function retreatDropdown(
e: HTMLElement,
closeAction: () => void = null
) {
e.style.maxHeight = `${e.offsetHeight}px`;
if (closeAction) setTimeout(() => closeAction(), 0);
}
Dropdown CSS class:
.dropdown {
transition: opacity 200ms linear, max-height 200ms linear;
will-change: opacity, max-height;
}
Angular ngStyle (applied on open/close of accordion):
elementStyle(e: HTMLElement) {
return this.isOpen(HTMLElement) // I use a map of booleans for open state here...
? { display: "block", "max-height": "none" }
: { "max-height": 0, opacity: 0 };
}

Vuejs how to pass image url as prop to be used as css variable for css animation

I have a component that has a hover animation where 4 images are rotated in a loop:
animation: changeImg-1 2.5s linear infinite;
#keyframes changeImg-1 {
0%, 100% { background-image: url('images/wel1.png'); }
25% { background-image: url('images/wel2.png'); }
50% { background-image: url('images/wel3.png'); }
75% { background-image: url('images/wel4.png'); }
}
Now I want to make this component reusable by being able to pass image strings in as props, those get assigned as css variables which then get picked up by the animation.
I got as far as defining the css variable with a path in a computed property which is then used in the css:
computed: {
userStyle () {
return {
'--myBackground': `url(${require('#/components/ImagesInText/images/wel1.png')})`,
}
}
},
CSS:
.image {
background:var(--myBackground);
}
What I can't get to work is to pick up an image path from props and use it in the computed property...
props: {
image: { type: String, default: '#/components/ImagesInText/images/wel1.png' },
},
If I do this below I get en error: Cannot find module '#/components/ImagesInText/images/wel1.png'"
computed: {
userStyle () {
return {
'--myBackground': `url(${require( this.image )})`,
}
}
},
When using a variable path/filename, require needs some assistance. You must hard code at least the first portion of the path as a string.
For example, this works:
const filename = 'wel1.png';
require('#/components/ImagesInText/images/' + filename); // <-- The string is needed
This works too:
const path = 'components/ImagesInText/images/wel1.png';
require('#/' + path); // <-- This is good enough too
But this would not work:
const fullpath = '#/components/ImagesInText/images/wel1.png';
require(fullpath); // <-- No. Can't infer the path from a variable only
As the way mentioned In this thread. it's working perfectly for me.
<div class="register-img" :style="{'background-image': 'url(' + require('#/assets/images/register-image.png') + ')'}"></div>

Performance concern with a React.js looping picture slideshow

We want to display a looping slideshow of pictures that looks like a gif. The current result is visible at this url: https://figuredevices.com.
Our current approach is using opacity to show or hide slides:
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
currentSlide: 0
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
let nextSlide = this.state.currentSlide + 1;
if (nextSlide == this.props.slides.length) {
nextSlide = 0;
}
this.setState({currentSlide: nextSlide});
}
render () {
let slides = this.props.pictures.map((picture, idx) => {
let slideContainerStyle = {
opacity: this.state.currentSlide == idx ? 1 : 0
};
return(
<div style={slideContainerStyle} key={idx}>
<Slide picture={picture}/>
</div>
);
})
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
{slides}
</div>
);
}
};
Pictures are loaded 5 by five into this.props.picture. The number of pictures is not bounded and I am worried about performance as this number grows. There are two things that don't feel right to me:
The map operation in the render method is traversing a whole array every 200ms only to change two css properties.
The DOM is growing a lot in size but most of nodes are hidden
Would you suggest a better approach, maybe using animation or react-motion ?
You should maintain all of your pictures as an array, and every 200ms, increment the array index. Then, instead of displaying all of the pictures every single time, just have it display the picture at your current index. This is a lot better because you're only returning one photo ever instead of a bunch of invisible ones :)
Note: I wasn't able to test this, so I'm not sure if everything is exactly syntactically correct, but this is the general idea. Every 200ms, increment the slideIndex. Then, always return a div with only the one picture you want to see.
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
slideIndex;
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
this.setState({this.state.slideIndex: (this.state.slideIndex + 1) % this.props.slides.length})
render () {
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
<Slide picture={this.props.pictures[this.state.currentSlide]}/>
</div>
);
}
};

How can I test for clip-path support?

clip-path:shape() does not seem to work in IE (no surprise) and Firefox (a bit surprised). Is there a way to test for clip-path support? I use modernizr. (By the way, I know I can get this to work using SVGs and -webkit-clip-path:url(#mySVG))
You asked this a while ago, and to be honest, I'm not sure if Modernizr has yet to add support for this, but it's pretty easy to roll your own test in this case.
The steps are:
Create, but do not append, a DOM element.
Check that it supports any kind of clipPath by checking the JS style attribute of the newly created element (element.style.clipPath === '' if it can support it).
Check that it supports CSS clip path shapes by making element.style.clipPath equal some valid CSS clip path shape.
Of course, it's a little more complex than that, as you have to check for vendor-specific prefixes.
Here it is all together:
var areClipPathShapesSupported = function () {
var base = 'clipPath',
prefixes = [ 'webkit', 'moz', 'ms', 'o' ],
properties = [ base ],
testElement = document.createElement( 'testelement' ),
attribute = 'polygon(50% 0%, 0% 100%, 100% 100%)';
// Push the prefixed properties into the array of properties.
for ( var i = 0, l = prefixes.length; i < l; i++ ) {
var prefixedProperty = prefixes[i] + base.charAt( 0 ).toUpperCase() + base.slice( 1 ); // remember to capitalize!
properties.push( prefixedProperty );
}
// Interate over the properties and see if they pass two tests.
for ( var i = 0, l = properties.length; i < l; i++ ) {
var property = properties[i];
// First, they need to even support clip-path (IE <= 11 does not)...
if ( testElement.style[property] === '' ) {
// Second, we need to see what happens when we try to create a CSS shape...
testElement.style[property] = attribute;
if ( testElement.style[property] !== '' ) {
return true;
}
}
}
return false;
};
Here's a codepen proof-of-concept: http://codepen.io/anon/pen/YXyyMJ
You can test with Modernizr.
(function (Modernizr) {
// Here are all the values we will test. If you want to use just one or two, comment out the lines of test you don't need.
var tests = [{
name: 'svg',
value: 'url(#test)'
}, // False positive in IE, supports SVG clip-path, but not on HTML element
{
name: 'inset',
value: 'inset(10px 20px 30px 40px)'
}, {
name: 'circle',
value: 'circle(60px at center)'
}, {
name: 'ellipse',
value: 'ellipse(50% 50% at 50% 50%)'
}, {
name: 'polygon',
value: 'polygon(50% 0%, 0% 100%, 100% 100%)'
}
];
var t = 0, name, value, prop;
for (; t < tests.length; t++) {
name = tests[t].name;
value = tests[t].value;
Modernizr.addTest('cssclippath' + name, function () {
// Try using window.CSS.supports
if ('CSS' in window && 'supports' in window.CSS) {
for (var i = 0; i < Modernizr._prefixes.length; i++) {
prop = Modernizr._prefixes[i] + 'clip-path'
if (window.CSS.supports(prop, value)) {
return true;
}
}
return false;
}
// Otherwise, use Modernizr.testStyles and examine the property manually
return Modernizr.testStyles('#modernizr { ' + Modernizr._prefixes.join('clip-path:' + value + '; ') + ' }', function (elem, rule) {
var style = getComputedStyle(elem),
clip = style.clipPath;
if (!clip || clip == "none") {
clip = false;
for (var i = 0; i < Modernizr._domPrefixes.length; i++) {
test = Modernizr._domPrefixes[i] + 'ClipPath';
if (style[test] && style[test] !== "none") {
clip = true;
break;
}
}
}
return Modernizr.testProp('clipPath') && clip;
});
});
}
})(Modernizr);
Check this codepen to see it in action.

detect if element is being transitioned

How to detect via js if any sort of transition is being applied to the element right now?
Short story of my problem:
I have a situation where I'm firing a function on the `transitionend` event, but sometimes the element doesn't have any *transition* being applied (because in Firefox, for example, the user might click some element rapidly which makes the transition goes crazy and stop working) so I want to know when it doesn't work and just fire the function myself, skipping the `transitionend`. I am trying to avoid ugly solutions..
You can use the Web Animation API for that, notably the Element#getAnimations method, which will return a list of Animation objects applied on the Element. These will include Web Animations (from .animate()), CSS #keyframes animations, and CSS transitions.
document.querySelectorAll("a").forEach((el) => {
el.onmouseenter = (evt) => {
const animations = el.getAnimations(); // we could also ask to look in the subtree
// we're only interested in CSS transitions
const transitions = animations.filter((anim) => anim instanceof CSSTransition);
console.log(transitions.length
? transitions.map((anim) => anim.transitionProperty )
: "no transition"
);
};
});
a:hover {
color: red;
opacity: 0.5;
}
.color-transition {
transition: color 1s;
}
.color-and-opacity-transition {
transition: color 1s, opacity 5s;
}
/*SO-only*/.as-console-wrapper { max-height: 110px !important }
<b>Hover these anchors to log their applied transitions.</b><br>
<a class="color-transition">color transition</a><br>
<a class="color-and-opacity-transition">color & opacity transition</a><br>
<a>no transition</a>
You can listen to transitionstart, transitionend, and, transitioncancel events. To tell if some element is under transition. However, you cannot know if some element will start a transition (even if it has transition-delay: 0s) using this code:
/** #type {Map<HTMLElement, number>} */
const transitionCounter = new Map();
/** #type {(() => void)[]} */
const waitingTransition = [];
const incReference = (counter, target) => {
if (counter.has(target)) {
counter.set(target, counter.get(target) + 1);
} else {
counter.set(target, 1);
}
};
const desReference = (counter, target) => {
if (!counter.has(target)) {
return;
} else if (counter.get(target) === 1) {
counter.delete(target);
} else {
counter.set(target, counter.get(target) - 1);
}
};
document.addEventListener('transitionstart', event => {
const { target } = event;
incReference(transitionCounter, target);
const onFinish = event => {
if (event.target !== target) return;
desReference(transitionCounter, target);
target.removeEventListener('transitioncancel', onFinish);
target.removeEventListener('transitionend', onFinish);
[...waitingTransition].forEach(listener => { listener(); });
};
target.addEventListener('transitioncancel', onFinish);
target.addEventListener('transitionend', onFinish);
});
/**
* #param {HTMLElement} element
* #returns {boolean}
*/
const isUnderTransition = function (element) {
const parents = [];
for (let i = element; i; i = i.offsetParent) parents.push(i);
return Array.from(transitionCounter.keys()).some(running => parents.includes(running));
};
/**
* #param {HTMLElement} element
* #returns {Promise<void>}
*/
const waitTransitionEnd = async function (element) {
if (!isUnderTransition(element)) return Promise.resolve();
return new Promise(resolve => {
waitingTransition.push(function listener() {
if (isUnderTransition(element)) return;
waitingTransition.splice(waitingTransition.indexOf(listener), 1);
resolve();
});
});
};
As specified by W3C Editor's Draft - CSS Transition
The ‘transitionend’ event occurs at the completion of the transition. In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire.
So, I think there's not a valid simple way to solve this problem. The solution is left to the implementation (the browser) which decide if it does or doesn't render the transition at all.
Maybe, a solution could be to attach a listener to the element that fires the transition and after a specific elapsed time it checks if the transitioned element has the required CSS attributes set, and if those attributes aren't set as expected you can run your function by yourself.

Resources