Update 2019-12-18 with better solution
See other SO question with updated solution
Trigger CSS transition on appended element
Short version: wrap the adding of a CSS animation class in a JS block that forces the browser to re-render the flow and not optimize it into a single call.
# CSS animation class
.visible {
...styles to change transparency from 0 -> 1
...styles to transform(scale) from 0.8 -> 1.0
}
# JS code
requestAnimationFrame(() => {
this.element.classList.add("visible")
})
Previously I had to do something like this:
$element.hide().show()
$element.addClass("visible")
Original question
I'm building an overlay (background for modals or dialog boxes) and I want it to fade in when I create the element. I do the animation by adding/removing a .visible class to the element using CSS3 transitions.
# SASS styles
.overlay {
background-color: rgba(0, 0, 0, 0.6);
cursor: pointer;
height: 100%;
left: 0;
position: fixed;
pointer-events: none;
top: 0;
width: 100%;
will-change: opacity;
#include transparency(0);
#include transition(opacity 0.3s cubic-bezier(0, 0, 0.3, 1));
&.visible {
pointer-events: auto;
#include transparency(1.0);
}
}
When the overlay element already exists on the DOM, everything works just fine:
$(".overlay").addClass("visible") # => does animation as expected...
However, when I CREATE the element and THEN try to animate it, it does not:
# JavasScript using jQuery
tag = $("<div class='overlay'></div>")
$("body").append(tag)
tag.addClass("visible")
I understand this is because the JavaScript is creating and adding the class "instantly", so what I have to do is this:
tag = $("<div class='overlay'></div>")
$("body").append(tag)
tag.hide()
tag.show()
tag.addClass("visible")
By "hiding" and then "showing" the element, it has enough time for the add class to animate the element.
Question
This seems pretty hacky to show/hide an element so I can then animate it via CSS transitions. Is there a cleaner way of implement this?
"Cleaner" is in the eye of the beholder, but one simple way is to wait a tick before you add the visible class:
$("body").append(tag);
setTimeout(function () {
tag.addClass("visible");
}, 0);
That'll wait until the appending is complete before adding the class. You might need to adjust the 0 value higher.
Related
I have a container with dynamic content a user can interact with. It could be a YouTube video (iframe), amongst other things.
A user can drag and drop an image into this container and it'll replace the dynamic content with their image.
I'm aware that due to the dynamic content, drag events will behave erratically in the container, so I need to do pointer-events: none.
The HTML container has the following classes ant-upload-drag and ant-upload-drag-hover. The ant-upload-drag-hover class is only active when the image being dragged is on-top of the container.
I have CSS akin to the following where ant-upload-drag-container contains the dynamic content that I want unreachable by pointer-events:
.ant-upload-drag-hover {
.ant-upload-drag-container {
pointer-events: none;
}
}
Unfortunately, this doesn't work. It causes the container to behave erratically with the ant-upload-drag-hover class added and removed from the container over and over and over.
Meanwhile, the following works:
.ant-upload-drag {
.ant-upload-drag-container {
pointer-events: none;
}
}
Essentially, as long as .ant-upload-drag-container is always pointer-events: none; things work as expected, but if the pointer-events: none is triggered dynamically on hover, then things don't go as planned.
The following video demonstrates visually:
https://share.getcloudapp.com/ApuRgQlZ
The first example shows what is desired (when pointer-events is always none). The second example shows the current state (when pointer-events is dynamically shifted to always none).
I know this isn't much to go off of, but I'm not sure if I fundamentally misunderstand pointer-events: none, or if some React rendering shenanigans are happening.
Presumably, the moment ant-upload-drag-hover class is added to the element, the pointer-events: none would trigger, but it seems like for some reason pointer-events: none is also triggered on the container or something else must be happening for the container to lose the hover state and remove the class.
I'm not sure whether pointer-events: none interferes with the drag-and-drop events but it would explain your bug.
I'd suggest that you instead put a layer on top of the container that catches these events instead.
Here an example with a button and the layer is toggles (by a class) every 3 seconds.
// toggle class `ant-upload-drag-hover` every 3 seconds
setInterval(() => {
document.querySelector(".ant-upload-drag").classList.toggle("ant-upload-drag-hover");
}, 3000);
.ant-upload-drag-container {
position: relative;
}
.ant-upload-drag-hover .ant-upload-drag-container::before {
content: "";
display: block;
position: absolute;
z-index: 999;
height: 100%;
width: 100%;
/* the rest is just for aesthetics: */
content: " .ant-upload-drag-hover";
background: rgba(255, 255, 255, .5);
cursor: not-allowed;
}
button {
padding: 50px 100px;
}
button::before {
content: "Click here: ";
}
<div class="ant-upload-drag">
<div class="ant-upload-drag-container">
<button onclick="++this.textContent">0</button>
</div>
</div>
I am using a hamburger menu which toggles class with jQuery and fades in/out a hidden menu using Javascript all in the same click function. It is working except that the icon has to be clicked twice before the menu fades in initially, not even double clicking gets it to work at the same time as the icon toggles class, after that it is fine and works as expected, just the delay on first opening page, Or is there a way to do it better with jQuery, I did try a couple of ways but couldn't get it. The code is below:
// In javascript
$( document ).ready(function() {
$(".hamburger").click(function() {
$("#primary_nav").toggleClass("is-active");
}
});
// In CSS
#primary_nav {
opacity: 0;
pointer-events: none;
transition: opacity 300ms;
}
#primary_nav.is-active {
opacity: 1;
pointer-events: auto;
}
Its all inside an init() function so Im not sure if I need the document ready. (I am using Sage 9 for Wordpress)
Any tips welcome. Thanks
Update I scrapped the javascript fades to use the jQuery toggle and css as in below answer, so its like that at the moment.
The original Javascript target the js-btn on click. Wordpress generates extra classes menu-main-container etc as in image below. Just need this little bit to finish the nav. Thanks
You asked for another way, so that's what I'm providing. I don't like doing simple transitions like this in JS - I find them easier and more performant in CSS. So here's my suggestion:
// In javascript
$( document ).ready(function() {
$(".hamburger").click(function() {
$("#primary_nav").toggleClass("is-active");
}
});
// In CSS
#primary_nav {
opacity: 0;
pointer-events: none;
transition: opacity 300ms;
}
#primary_nav.is-active {
opacity: 1;
pointer-events: auto;
}
#media (min-width: 920px){
#primary_nav {
opacity: 1;
pointer-events: auto;
}
}
I'm experimenting with a design pattern using .expands to toggle .hidden child classes for .expanded auto-sized flex box elements.
Something like:
const expandClick = e => {
e.currentTarget.querySelectorAll('.expanded').forEach(child => {
child.classList.toggle('hidden')
})
}
document.querySelectorAll('.expands').forEach(expandable => {
expandable.addEventListener('click', expandClick)
})
https://jsfiddle.net/vpc8khq8/
When my inner content loses the display: none attribute, I would like the height to ease out with a slight bounce, but I'm not quite sure how to pull that off with a vanilla CSS transition or a pinch of JS.
I came across this post: How can I transition height: 0; to height: auto; using CSS?
But many of the answers seem more like hacks with odd side effects and excess js / css than simple, lightweight solutions.
This does the trick with jQuery: https://jsfiddle.net/cm7a9jr7/
const expandClick = e => {
$(e.currentTarget).children('.expanded').each(function() {
$(this).slideToggle('slow', 'swing', function() {
$(this).toggleClass('hidden')
})
})
}
$(() => $('.expands').on('click', expandClick))
But, I'd like to use something leaner. Are the any recommended libraries or simple ways to pull this off in CSS?
I've updated your fiddle https://jsfiddle.net/vpc8khq8/2/
Instead of using display none you can get a transition effect by setting max-height to 0 on the hidden section then change the max-height to a fixed height (greater than the section will ever be) to reveal. This also requires you to set overflow hidden on the parent element:
.expands{overflow: hidden;}
section.hidden {
max-height: 0;
background: red;
}
section{
transition: 1s cubic-bezier(0.35, 1.17, 0.39, -0.61);
max-height: 400px;
background: blue;
}
I've created a semi-decent cubic-bezier to get the bounce effect, though with a bit of refinement you could probably get something better.
If you open chrome devtools and click on the transition icon you can select from a number of presets and also create your own by moving the curve points around. Devtools will show you a preview of the transition and you can test it out without reloading. Then just copy that cubic-bezier code back to your CSS.
To take it a step further you could achieve all of this without any JS, using hidden checkboxes and some CSS trickery along the lines of this example: https://codepen.io/mrosati84/pen/AgDry
I want to animate a translateX with transition on a click event by adding a class to the div in the js. The transform and transition properties are added in the css file.
var widget = document.getElementById('widget');
widget.style.display = 'block';
document.getElementById('widget2').clientWidth; //comment this line out and it wont work
widget.className = 'visible';
It only works if I query the width property of any element in the dom before adding the class.
here is a jsfiddle:
https://jsfiddle.net/5z9fLsr5/2/
Can anyone explain why this is not working?
That's because you begin your transition and modified the display property "at the same time". Altering display will ruin any transition (citation needed, admittedly), so it would be a good idea to isolate the display changing and actual transiting:
https://jsfiddle.net/5z9fLsr5/3/
document.getElementById('showWidget').addEventListener('click', function(e) {
e.preventDefault();
var widget = document.getElementById('widget');
widget.style.display = 'block';
//document.getElementById('widget2').clientWidth;
window.setTimeout(function(){
widget.className = 'visible';
},0);
});
#widget {
width: 200px;
height: 80px;
background: black;
position: absolute;
transition: transform 500ms;
transform: translateX(-200px);
display: none;
}
#widget.visible {
transform: translateX(200px);
}
#widget2 {
position: absolute;
right: 0
}
show
<div id="widget"></div>
<div id="widget2">xxx</div>
Querying clientWidth seems to "pause" the execution for some time, so it works too.
The issue here is the initial setting of display: none. To the browser's layout manager, this indicates that the layout should be done as if the element in question wasn't even in the DOM (it still is, mind you). This means that the CSS style transform: translateX(-200px); will not be applied.
Doing this:
widget.style.display = 'block';
widget.className = 'visible';
triggers both modifications essentially at the same time - the layout is only re-done after both statements have been executed. Inserting document.getElementById('widget2').clientWidth; (clientHeight works as well) triggers the layout manager to repaint, thus registering transform: translateX(-200px).
As others have mentioned before me, the solution is to either use opacity instead of display (this would be my choice), or to use setTimeout with a delay of 0 (see Why is setTimeout(fn, 0) sometimes useful?).
I have a couple of classes: hide is display: none, and transparent is opacity: 0. The element pr_container has -webkit-transition: opacity 1s. The following JQuery-based code makes an element appear in an animated fasion:
pr_container.removeClass("hide");
setTimeout(function() { pr_container.removeClass("transparent"); }, 0);
However, when I remove setTimeout and instead just remove the second class, there is no animation. Why?
Edit: I'm using the latest Chrome, I haven't checked other browsers yet.
Edit: I tried putting both calls in the same setTimeout callback - no animation. So it's clearly about separation.
Edit: here's the jsFiddle: http://jsfiddle.net/WfAVj/
You can't make a transition if you are changing display property at the same time. So in order to make it work you have to hide your element some other way. For example:
.hide {
height: 0;
width: 0;
/* overflow: hidden; padding: 0; border: none; */
}
http://jsfiddle.net/dfsq/WfAVj/1/
There's no reasonable "curve" to transit from one display status to another, so in current implementation of browsers, any transition that somehow involves display will end up with no transition at all.
With this code:
pr_container.removeClass("hide");
pr_container.removeClass("transparent");
You can imagine the two statements execute in a single "blocking" queue, so browsers practically renders the element from class="hide transparent" to class="", and as stated above, the hide class practically invalidates any existing transition.
By using
pr_container.removeClass("hide");
setTimeout(function() { pr_container.removeClass("transparent"); }, 0);
You told browsers to remove the "transparent" class "as soon as possible, but no in the same queue", so browser first removes "hide", and then moves on. The removal of "transparent" happens when the browser think it has resource to spare, thus the transition does not get invalidated.
only the "transperent" class produce animation .. "hide" is instant. So start the animation and if needed "hide" after 1 second:
test.addClass("transparent");
//hide after 1 sec, when the animation is done
setTimeout(function() {test.addClass("hide"); }, 1000); //1000ms = 1sec
http://jsfiddle.net/WfAVj/4/
By using suggestions in the linked question, I made a version that I'm satisfied with:
.test {
-webkit-transition: visibility 1s, opacity 1s;
}
.hide {
visibility: hidden;
}
.transparent {
opacity: 0;
}
http://jsfiddle.net/xKgjS/
Edit: now the two classes can even be combined to one!
Thanks to everyone!