RichardTape
RichardTape

Reputation: 21713

Transitions on the CSS display property

I'm currently designing a CSS 'mega dropdown' menu - basically a regular CSS-only dropdown menu, but one that contains different types of content.

At the moment, it appears that CSS 3 transitions don't apply to the 'display' property, i.e., you can't do any sort of transition from display: none to display: block (or any combination).

Is there a way for the second-tier menu from the above example to 'fade in' when someone hovers over one of the top level menu items?

I'm aware that you can use transitions on the visibility: property, but I can't think of a way to use that effectively.

I've also tried using height, but that just failed miserably.

I'm also aware that it's trivial to achieve this using JavaScript, but I wanted to challenge myself to use just CSS, and I think I'm coming up a little short.

Upvotes: 2006

Views: 2566781

Answers (30)

EzioMercer
EzioMercer

Reputation: 2007

Information was taken from this video

Now at least in Chrome 117 you can animate display property:

@keyframes method

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body,
html {
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  justify-content: center;
  align-items: center;
  background-color: gray;
}

div {
  width: 100px;
  height: 100px;
  background-color: red;
}

div:nth-child(2) {
  animation: open 2s;
}

body:hover > div:nth-child(2) {
  display: none;
  opacity: 0;
  animation: close 2s;
}

@keyframes open {
  from {
    opacity: 0;
    display: none;
  }
  
  to {
    opacity: 1;
    display: block;
  }
}

@keyframes close {
  from {
    opacity: 1;
    display: block;
  }
  
  to {
    opacity: 0;
    display: none;
  }
}
<div>1</div>
<div>2</div>
<div>3</div>

transition method

To be able to use transition you need two new things:

  • transition-behavior - this allows you to animate discrete properties
  • @starting-style - this allows you to use transition in both directions (try without this and you will see difference)

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body,
html {
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  justify-content: center;
  align-items: center;
  background-color: gray;
}

div {
  width: 100px;
  height: 100px;
  background-color: red;
}

div:nth-child(2) {
  @starting-style {
    opacity: 0;
  }
  
  transition-property: display, opacity;
  transition-duration: 2s;
  transition-behavior: allow-discrete;
}

body:hover > div:nth-child(2) {
  display: none;
  opacity: 0;
}
<div>1</div>
<div>2</div>
<div>3</div>

Upvotes: 3

vsync
vsync

Reputation: 130421

transition-behavior: allow-discrete

It is now possible to specify allow-discrete for a transitioned display property so it will only "transition" after a certain amount of time, thus ensuring the rest of the transitioned properties are taking effect:

  • When animating display from none to block (or another visible display value), the value will flip to block at 0% of the animation duration so it is visible throughout.
  • When animating display from block (or another visible display value) to none, the value will flip to none at 100% of the animation duration so it is visible throughout.

Shorthand use case:

transition: opacity 0.5s, display 0.5s allow-discrete;

enter image description here

Full example:

label {
  display: flex;
  place-items: center;
  user-select: none;
  cursor: pointer;
}

label:nth-of-type(1):has(:checked) ~ ul > li:nth-of-type(1) {
  opacity: 0;
  padding: 0;
  display: none;
}

label:nth-of-type(2):has(:checked) ~ ul > li:nth-of-type(2) {
  opacity: 0;
  padding: 0;
  display: none;
}

ul {
  list-style: none;
  display: flex;
  place-items: center;
  gap: 1em;
}

li {
  padding: 80px;
  display: block;
  transition: padding 0.3s, opacity 0.3s, display 0.3s;
}

/* rules are to be set as beginning-state for transitioning 
   from "display:none" to a visible display:
   https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style
*/
@starting-style {
  li {
    opacity: 0;
    padding: 0;
  }
}

ul > li:nth-of-type(1) {
  transition-behavior: allow-discrete;
}
<label><input type=checkbox> Hide green card (with `allow-discrete`)</label>
<label><input type=checkbox> Hide blue card</label>

<ul>
  <li style='background:PaleGreen'></li>
  <li style='background:CadetBlue'></li>
  <li style='background:RosyBrown'></li>
</ul>

For the above example I've set transition-behavior: allow-discrete; only for the first card (number 1) and when the Hide first card checkbox is checked, the card will not vanish immediately just because it now has display: none; but will "wait" 0.25ms before applying it.

@starting-style

Notice the above code also uses @starting-style which are needed to instruct the browser which initials styles are applied to an element once it is a part of the DOM, since display:none elements aren't a part of the DOM even if they appear in the markup.

Using @starting-style is only needed for transitioning-in from display:none to whatever, but unneeded if only a transitioning-out is required.

enter image description here

Further reading:

Upvotes: 14

Marc
Marc

Reputation: 5465

I started an open source skeleton project called Toggle Display Animate.

This skeleton helper will allow you to easily mimic jQuery show/hide, but with in/out CSS 3 transition animations.

It uses class toggles so you can use any CSS methods you want on elements besides display:none|block|table|inline, etc. as well as other alternate uses that can be thought up.

Its main design purpose is for element toggle states, and it supports a revert state where hiding the object allows you to run your keyframe in reverse or play an alternate animation for hiding the element.

Most of the markup for the concept I am working on is CSS, and there is very little JavaScript actually used.

Upvotes: 2

Zolt&#225;n Bata
Zolt&#225;n Bata

Reputation: 81

My solution is similar like the actual best answer but I use the max-height too, becouse the visibility hidden is not enough sometimes

const paneltrigger = document.querySelector("#paneltrigger")
const panel = document.querySelector("#panel")

paneltrigger.addEventListener('click', ()=>{
    const isOpen = paneltrigger.getAttribute("aria-expanded") === "false" ?  false : true;
    
    if(isOpen){
        paneltrigger.setAttribute("aria-expanded", "false");
        panel.classList.remove('opened')
    }else{
        paneltrigger.setAttribute("aria-expanded", "true");
        panel.classList.add('opened')
    }
})
#panel {
    background-color: red;
    opacity: 0;
    max-height: 0;
    overflow: hidden;
    visibility: hidden;
    width: 100%;
    transition: max-height 1s, visibility 1s, opacity 1s ease
}

#panel.opened {
    opacity: 1;
    max-height: 500000px;
    overflow: auto;
    visibility: visible;
}
<button id="paneltrigger" aria-expanded="false">SHOW / HIDE</button>
<div id="panel">CONTENT</div>

Upvotes: 1

Arsen Kazydub
Arsen Kazydub

Reputation: 5660

Instead of callbacks, which don't exist in CSS, we can use transition-delay property.

#selector {
    overflow: hidden; /* Hide the element content, while height = 0 */
    height: 0;
    opacity: 0;
    transition: height 0ms 400ms, opacity 400ms 0ms;
}
#selector.visible {
    height: 100px; /* any measurable value, not "auto" */
    opacity: 1;
    transition: height 0ms 0ms, opacity 400ms 0ms;
}

So, what's going on here?

  1. When .visible class is added, both height and opacity start animation without delay (0ms), though height takes 0ms to complete animation (equivalent of display: block) and opacity takes 400ms.

  2. When .visible class is removed, opacity starts animation (0ms delay, 400ms duration), and height waits 400ms and only then instantly (0ms) restores the initial value (equivalent of display: none in the animation callback).

Note, this approach is better than ones using visibility. In such cases, the element still occupies the space on the page, and it's not always suitable.

For more examples please refer to this article.

Upvotes: 167

MDR
MDR

Reputation: 311

Update for 2023

The W3C has made a addition/change to the spec that makes display animatable.

Starting with Chrome 116 you can use the following

.card { animation: fade-out 0.5s forwards; }
@keyframes fade-out { 100% { opacity: 0; display: none; } 

Upvotes: 1

Janspeed
Janspeed

Reputation: 2814

I didn't get absolute positioning to work properly and all I wanted was to removed the space of the element when it was hidden so I thought I would add my solution to the pool in case someone else is looking for something similar.

In summary what I did was to also transition the height. The key is delaying that transition until after the opacity had completed.

.transitionFadeWrapper {
    visibility: hidden;
    opacity: 0;
    height: 0;
    transition: visibility 0s linear 300ms, opacity 300ms ease-out, height 0s linear 300ms;
}

.transitionFadeWrapper:hover {
    visibility: visible;
    opacity: 1;
    height: fit-content;
    transition-delay: 0s;
}

Maybe something similar can be used to animate the menu in your case. Hope this help.

Upvotes: 0

nCardot
nCardot

Reputation: 6595

Fade it in with CSS Animations:

const SECONDS = 1000

setTimeout(() => {
  const item = document.querySelector('.item')
  item.classList.add('shown')
}, 1 * SECONDS)
.item {
  display: none;
}

.item.shown {
  display: block;
  animation: fadeIn 0.5s;
}

@keyframes fadeIn {
  from {
      opacity: 0;
  }

  to {
      opacity: 1;
  }
}
<div class="item">
  hello world
</div>

Upvotes: 38

Louis Stekhoven-Smith
Louis Stekhoven-Smith

Reputation: 1189

Example of "hiding" an item within a flex box with a transition without using the display property

As others mention it is not possible to trigger a transition on an element you just changed from display: none; to display: block; but you can achieve the same effect using overflow: hidden

This answer uses what I learnt from Jim Jeffers answer - https://stackoverflow.com/a/3332179/7961500

However I wanted to transition on translateX and within a flex container.

To get this working as expected you need to set the following on the item you want to hide and transition.

  overflow: hidden;
  flex-basis: 0;
  flex-grow: 0;
  height: 0;
  transition: transform 0.3s ease-in-out;
  transform: translateX(150%);

Set this css when you want to unhide

  flex-grow: 1;
  height: auto;
  overflow: visible;

And set this to trigger the transition

  transform: translateX(0);

However you must decouple hiding/showing from triggering the actual transition so that when you hide the element the transition can first be played before you hide it.

Check this example out

.container {
  display: flex;
  justify-content: center;
}

.content {
  flex: 1 0 0;
}
.sidebar {
  overflow: hidden;
  flex-basis: 0;
  flex-grow: 0;
  height: 0;
  transition: transform 0.5s ease-in-out;
  transform: translateX(150%);
}

.sidebar.show {
  transform: translateX(0);
}

.sidebar.expand-content {
  flex-grow: 1;
  height: auto;
  overflow: visible;
}
<div class='container'>
  <div class='content'>item1</div>
  <div class='content'>item2</div>
  <div class='content'>item3</div>
  <div class='content'>item4</div>
  <div class='content'>item5</div>
  <div class='sidebar'>side bar withexpandingcontentthattakes up space</div>
</div>

<button onclick="showNav()">Show nav</button>
<button onclick="hideNav()">Hide nav</button>
  <script>
    function showNav() {
      document.querySelector('.sidebar').classList.add('expand-content');
      document.querySelector('.sidebar').classList.add('show');
    }
    function hideNav() {
  document.querySelector('.sidebar').classList.remove('show');
    }
    
    //don't remove the elements space untill the transition has finished
     const sidebar = document.querySelector('.sidebar');
sidebar.addEventListener("transitionend", () => {
    if (!sidebar.classList.contains('show')) {
        sidebar.classList.remove('expand-content');
      }
})
    </script>

React tailwind example

  const FilterSideBar = (props: Props) => {
      const filterSideBar = useRef<HTMLDivElement>(null)
    
      useEffect(() => {
        if (!filterSideBar.current) {
          return
        }
        // Uncomment if we want to change sidebar to hidden by default.
        // filterSideBar.current.classList.add("overflow-hidden", "grow-0", "basis-0", "h-0")
        // filterSideBar.current.classList.remove("ml-6", "min-w-min")
    
        filterSideBar.current.addEventListener("transitionend", (event) => {
          if (!filterSideBar.current || event.target !== filterSideBar.current) {
            return;
          }
          if (filterSideBar.current.classList.contains("translate-x-[150%]")) {
            filterSideBar.current.classList.add("overflow-hidden", "grow-0", "basis-0", "h-0")
            filterSideBar.current.classList.remove("ml-6")
          }
        })
    
        filterSideBar.current.addEventListener("transitionstart", (event) => {
          if (!filterSideBar.current || event.target !== filterSideBar.current) {
            return;
          }
          if (filterSideBar.current.classList.contains("translate-x-0")) {
            filterSideBar.current.classList.remove("overflow-hidden", "grow-0", "basis-0", "h-0")
          }
        })
      }, [])
      return (
        <div
          ref={filterSideBar}
          className={`${props.isOpen ? "translate-x-0" : "translate-x-[150%]"} flex-none transform transition-transform duration-300 ease-in-out`}
        > 
            side nav
        </div>
    )
}

Upvotes: 1

Jez
Jez

Reputation: 30063

I've found the best solution, overall, to be: use a negative top margin instead of display:none.

The solution that worked well for me was to use a negative top margin - margin being valid for animation/transition - to shift the menu off the top of the page after my transform transition. One of the issues for me was that it was taking up space even after being hidden using visibility, and this solution below (combined with absolute positioning) solved that problem. Doing this (and setting visibility to hidden for semantic purposes) is almost as good as giving it display:none. Note, however, that the visibility setting below is optional, and the real work is being done by the negative margin.

I like this solution because it avoids JavaScript - you can see I'm using an input element (hidden underneath a burger icon) to hide/unhide the menu which slides in from the left of the screen. The one slight annoyance is having to use an "arbitrarily large number" for the negative margin, but it works well enough. The only fully clean solution IMHO is for transitions to support things like display where the transition just waits for the delay then transitions instantly, but with the current CSS standards spec here is my best solution:

CSS

#menu {
    /* Hide menu */
    position: absolute;
    overflow: hidden;

    margin: -99999px 0 0 -50px;
    padding: 0;
    visibility: hidden;
    transform-origin: 0% 0%;
    transform: translate(-100%, 0);

    transition: margin 0s 0.5s, padding 0s 0.5s, visibility 0s 0.5s, transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);
}
#menuToggle input:checked~#menu {
    /* Display menu */
    overflow: unset;

    margin: -80px 0 0 -50px;
    padding: 125px 50px 25px 50px;
    visibility: unset;
    transform: none;

    transition: margin 0s 0s, padding 0s 0s, visibility 0s 0s, transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);
}

HTML

<div id="menuToggle">
    <input type="checkbox" id="chkBurgerMenu" />
    <span></span><span></span><span></span>
    <ul class="navbar-nav">
        <li><a href="#" class="nav-link"">Home</a></li>
        <li><a href="#" class="nav-link"">Link1</a></li>
        <li><a href="#" class="nav-link"">Link2</a></li>
    </ul>
</div>

Upvotes: 1

Andres Paul
Andres Paul

Reputation: 1078

In my case I was using the Jquery toggle function to manage the visibility of the element, so instead of handling the transition with css I used to slideToggle to manage the visibility and to handle the transition part too.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>slideToggle demo</title>
  <style>
  p {
    width: 400px;
  }
  </style>
  <script src="https://code.jquery.com/jquery-3.5.0.js"></script>
</head>
<body>
 
<button id="toggle-button">Toggle</button>
<button id="slide-toggle-button">Slide Toggle</button>
<p>
  This is the paragraph to end all paragraphs.  You
  should feel <em>lucky</em> to have seen such a paragraph in
  your life.  Congratulations!
</p>
 
<script>
$( "#toggle-button" ).click(function() {
   $( "p" ).toggle();
});
$( "#slide-toggle-button" ).click(function() {
  $( "p" ).slideToggle( "slow" );
});
</script>
 
</body>
</html>

Upvotes: 1

Roman Grinev
Roman Grinev

Reputation: 1037

I appreciate all the answers. Here is what I'm using for similar purposes: transition vs animation.

Example: https://jsfiddle.net/grinevri/tcod87Le/22/

<div class="animation"></div>
<div class="transition"></div>
@keyframes animationTo {
  0% { background-color: rgba(0, 0, 0, 0.1); }
  100% { background-color: rgba(0, 0, 0, 0.5); }
}

@keyframes animationFrom {
  0% { background-color: rgba(0, 0, 0, 0.5); }
  100% { background-color: rgba(0, 0, 0, 0.1); }
}

.animation,
.transition{
  margin: 5px;
  height: 100px;
  width: 100px;
  background-color: rgba(0, 0, 0, 0.1);
}

.animation{
  animation: animationFrom 250ms;
}

.animation:hover{
  background-color: rgba(0, 0, 0, 0.5);
  animation: animationTo 250ms;
}

.transition{
  transition: background-color 250ms;
}

.transition:hover{
  background-color: rgba(0, 0, 0, 0.5);
}

Upvotes: 7

ThisIsWilliam
ThisIsWilliam

Reputation: 1115

I found better way for this issue, you can use CSS Animation and make your awesome effect for showing items.

.item {
     display: none;
}

.item:hover {
     display: block;
     animation: fade_in_show 0.5s
}

@keyframes fade_in_show {
     0% {
          opacity: 0;
          transform: scale(0)
     }

     100% {
          opacity: 1;
          transform: scale(1)
     }
}

Upvotes: 69

AdibTE
AdibTE

Reputation: 151

Well another way to apply transition in this situation without using keyframes is to set the width of your element to zero and then unset it on hover

.className{
  visibility:hidden;
  opacity: 0;
  transition: .2s;
  width:0;
}

.className:hover{
  visibility:visible;
  margin-right: .5rem;
  opacity: 1;
  width:unset;
}

Upvotes: 8

Joel_MMCC
Joel_MMCC

Reputation: 1531

I suspect that the reason that transitions are disabled if display is changed is because of what display actually does. It does not change anything that could conceivably be smoothly animated.

display: none; and visibility: hidden; are two entirely different things.
Both do have the effect of making the element invisible, but with visibility: hidden; it’s still rendered in the layout, but just not visibly so.
The hidden element still takes up space, and is still rendered inline or as a block or block-inline or table or whatever the display element tells it to render as, and takes up space accordingly.
Other elements do not automatically move to occupy that space. The hidden element just doesn’t render its actual pixels to the output.

display: none on the other hand actually prevents the element from rendering entirely.
It does not take up any layout space.
Other elements that would’ve occupied some or all of the space taken up by this element now adjust to occupy that space, as if the element simply did not exist at all.

display is not just another visual attribute.
It establishes the entire rendering mode of the element, such as whether it’s a block, inline, inline-block, table, table-row, table-cell, list-item, or whatever!
Each of those have very different layout ramifications, and there would be no reasonable way to animate or smoothly transition them (try to imagine a smooth transition from block to inline or vice-versa, for instance!).

This is why transitions are disabled if display changes (even if the change is to or from nonenone isn’t merely invisibility, it’s its own element rendering mode that means no rendering at all!).

Upvotes: 123

Chris Moschini
Chris Moschini

Reputation: 37967

You can get this to work the natural way you expected - using display - but you have to throttle the browser to get it to work, using either Javascript or as others have suggested a fancy trick with one tag inside another. I don't care for the inner tag as it further complicates CSS and dimensions, so here's the Javascript solution:

https://jsfiddle.net/b9chris/hweyecu4/17/

Starting with a box like:

<div id="box" class="hidden">Lorem</div>

A hidden box.

div.hidden {
    display: none;
}
#box {
    transition: opacity 1s;
}
    

We're going to use a trick found in a related q/a, checking offsetHeight to throttle the browser instantaneously:

https://stackoverflow.com/a/16575811/176877

First, a library formalizing the above trick:

$.fn.noTrnsn = function () {
    return this.each(function (i, tag) {
        tag.style.transition = 'none';
    });
};
$.fn.resumeTrnsn = function () {
    return this.each(function (i, tag) {
        tag.offsetHeight;    
        tag.style.transition = null;
    });
};

Next, we're going to use it to reveal a box, and fade it in:

$('#button').on('click', function() {
    var tag = $('#box');
    if (tag.hasClass('hidden'))
        tag.noTrnsn().removeClass('hidden')
        .css({ opacity: 0 })
        .resumeTrnsn().css({ opacity: 1 });
    else
        tag.css({ opacity: 0 });
});

This fades the box in and out. So, .noTrnsn() turns off transitions, then we remove the hidden class, which flips display from none to its default, block. We then set opacity to 0 to get ready for fading in. Now that we've set the stage, we turn transitions back on, with .resumeTrnsn(). And finally, kick off the transition by setting opacity to 1.

Without the library, both the change to display and the change to opacity would've gotten us undesirable results. If we simply removed the library calls, we'd get no transitions at all.

Note that the above does not set display to none again at the end of the fadeout animation. We can get fancier though. Let's do so with one that fades in and grows in height from 0.

Fancy!

https://jsfiddle.net/b9chris/hweyecu4/22/

#box {
    transition: height 1s, opacity 1s;
}

We're now transitioning both height and opacity. Note that we are not setting height, which means it is the default, auto. Conventionally this cannot be transitioned - moving from auto to a pixel value (like 0) will get you no transition. We're going to work around that with the library, and one more library method:

$.fn.wait = function (time, fn) {
    if (time)
        this.delay(time);
    if (!fn)
        return this;

    var _this = this;
    return this.queue(function (n) {
        fn.call(_this);
        n();
    });
};

This is a convenience method that lets us participate in jQuery's existing fx/animation queue, without requiring any of the animation framework that's now excluded in jQuery 3.x. I'm not going to explain how jQuery works, but suffice to say, the .queue() and .stop() plumbing that jQuery provides help us prevent our animations from stepping on each other.

Let's animate the slide down effect.

$('#button').on('click', function() {
    var tag = $('#box');
    if (tag.hasClass('hidden')) {
        // Open it
        // Measure it
        tag.stop().noTrnsn().removeClass('hidden').css({
            opacity: 0, height: 'auto'
        });
        var h = tag.height();
        tag.css({ height: 0 }).resumeTrnsn()
        // Animate it
        .css({ opacity: 1, height: h })
        .wait(1000, function() {
            tag.css({ height: 'auto' });
        });
    } else {
        // Close it
        // Measure it
        var h = tag.noTrnsn().height();
        tag.stop().css({ height: h })
        .resumeTrnsn()
        // Animate it
        .css({ opacity: 0, height: 0 })
        .wait(1000, function() {
            tag.addClass('hidden');
        });
    }
});

This code begins by checking on #box and whether it's currently hidden, by checking on its class. But it accomplishes more using the wait() library call, by adding the hidden class at the end of the slideout/fade animation, which you'd expect to find if it is in fact hidden - something the above simpler example could not do. This happens to also enable display/hiding the element over and over, which was a bug in the previous example, because the hidden class was never restored.

You can also see CSS and class changes being called after .noTrnsn() to generally set the stage for animations, including taking measurements, like measuring what will be the final height of #box without showing that to the user, before calling .resumeTrnsn(), and animating it from that fully-set stage to its goal CSS values.

Old Answer

https://jsfiddle.net/b9chris/hweyecu4/1/

You can have it transition on click with:

function toggleTransition() {
  var el = $("div.box1");

  if (el.length) {
    el[0].className = "box";
    el.stop().css({maxWidth: 10000}).animate({maxWidth: 10001}, 2000, function() {
        el[0].className = "box hidden";
    });
  } else {
    el = $("div.box");
    el[0].className = "box";
    el.stop().css({maxWidth: 10001}).animate({maxWidth: 10000}, 50, function() {
        el[0].className = "box box1";
    });
  }

  return el;
}

someTag.click(toggleTransition);

The CSS is what you'd guess:

.hidden {
    display: none;
}
.box {
    width: 100px;
    height: 100px;
    background-color: blue;
    color: yellow;
    font-size: 18px;
    left: 20px;
    top: 20px;
    position: absolute;
    -webkit-transform-origin: 0 50%;
    transform-origin: 0 50%;
    -webkit-transform: scale(.2);
    transform: scale(.2);
    -webkit-transition: transform 2s;
    transition: transform 2s;
}
.box1{
    -webkit-transform: scale(1);
    transform: scale(1);
}

The key is throttling the display property. By removing the hidden class and then waiting 50 ms, then starting the transition via the added class, we get it to appear and then expand like we wanted, instead of it just blipping onto the screen without any animation. Similar occurs going the other way, except we wait till the animation is over before applying hidden.

Note: I'm abusing .animate(maxWidth) here to avoid setTimeout race conditions. setTimeout is quick to introduce hidden bugs when you or someone else picks up code unaware of it. .animate() can easily be killed with .stop(). I'm just using it to put a 50 ms or 2000 ms delay on the standard fx queue where it's easy to find/resolve by other coders building on top of this.

Upvotes: 4

vSimak
vSimak

Reputation: 385

It is as simple as the following :)

@keyframes fadeout {
    0% { opacity: 1; height: auto; }
    90% { opacity: 0; height: auto; }
    100% { opacity: 0; height: 0;
}
animation: fadeout linear 0.5s 1 normal forwards !important;

Get it to fade away, and then make it height 0;. Also make sure to use forwards so that it stays in the final state.

Upvotes: 10

Miguel Q
Miguel Q

Reputation: 3628

You can do this with transition events, so you build two CSS classes for the transition, one holding the animation other, holding the display none state. And you switch them after the animation is ended? In my case I can display the divs again if I press a button, and remove both classes.

Try the snippet below...

$(document).ready(function() {
  // Assign transition event
  $("table").on("animationend webkitAnimationEnd", ".visibility_switch_off", function(event) {
    // We check if this is the same animation we want
    if (event.originalEvent.animationName == "col_hide_anim") {
      // After the animation we assign this new class that basically hides the elements.
      $(this).addClass("animation-helper-display-none");
    }

  });

  $("button").click(function(event) {

    $("table tr .hide-col").toggleClass(function() {
      // We switch the animation class in a toggle fashion...
      // and we know in that after the animation end, there
      // is will the animation-helper-display-none extra
      // class, that we nee to remove, when we want to
      // show the elements again, depending on the toggle
      // state, so we create a relation between them.
      if ($(this).is(".animation-helper-display-none")) {
        // I'm toggling and there is already the above class, then
        // what we want it to show the elements , so we remove
        // both classes...
        return "visibility_switch_off animation-helper-display-none";
      }
      else {
        // Here we just want to hide the elements, so we just
        // add the animation class, the other will be added
        // later be the animationend event...
        return "visibility_switch_off";
      }
    });
  });
});
table th {
  background-color: grey;
}

table td {
  background-color: white;
  padding: 5px;
}

.animation-helper-display-none {
  display: none;
}

table tr .visibility_switch_off {
  animation-fill-mode: forwards;
  animation-name: col_hide_anim;
  animation-duration: 1s;
}

@-webkit-keyframes col_hide_anim {
  0% {opacity: 1;}
  100% {opacity: 0;}
}

@-moz-keyframes col_hide_anim {
  0% {opacity: 1;}
  100% {opacity: 0;}
}

@-o-keyframes col_hide_anim {
  0% {opacity: 1;}
  100% {opacity: 0;}
}

@keyframes col_hide_anim {
  0%   {opacity: 1;}
  100% {opacity: 0;}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
  <theader>
    <tr>
      <th>Name</th>
      <th class='hide-col'>Age</th>
      <th>Country</th>
    </tr>
  </theader>
  <tbody>
    <tr>
      <td>Name</td>
      <td class='hide-col'>Age</td>
      <td>Country</td>
    </tr>
  </tbody>
</table>

<button>Switch - Hide Age column with fadeout animation and display none after</button>

Upvotes: 3

Christophe Marois
Christophe Marois

Reputation: 6729

This solution has excellent compatibility, and I haven't seen it yet:

.hidden-element {
  position: absolute;
  z-index: -1;
  pointer-events: none;
  visibility: hidden;
  opacity: 0;
  transition: visibility 0s, opacity .5s ease-out;
}

.hidden-element.visible {
  position: static;
  z-index: auto;
  pointer-events: auto;
  visibility: visible;
  opacity: 1;
}

Explanation: it uses the visibility: hidden trick (which is compatible with “show-and-animate” in one step), but it uses the combination position: absolute; z-index: -1; pointer-events: none; to make sure that the hidden container does not take space and does not answer to user interactions.

Upvotes: 3

svnm
svnm

Reputation: 24378

I think SalmanPK has the closest answer. It does fade an item in or out, with the following CSS animations. However, the display property does not animate smoothly, only the opacity.

@-webkit-keyframes fadeIn {
    from { opacity: 0; }
      to { opacity: 1; }
}

@-webkit-keyframes fadeOut {
    from { opacity: 1; }
      to { opacity: 0; }
}

If you want to animate the element moving from display block to display none, I can't see that it is currently possible just with CSS. You have to get the height and use a CSS animation to decrease the height. This is possible with CSS as shown in the example below, but it would be tricky to know the exact height values you need to animate for an element.

jsFiddle example

CSS

@-webkit-keyframes pushDown {
  0% {
    height: 10em;
  }
  25% {
    height: 7.5em;
  }
  50% {
    height: 5em;
  }
  75% {
    height: 2.5em;
  }
  100% {
    height: 0em;
  }
}

.push-down {
    -webkit-animation: pushDown 2s forwards linear;
}

JavaScript

var element = document.getElementById("element");

// Push item down
element.className = element.className + " push-down";

Upvotes: 3

Helmut Emmelmann
Helmut Emmelmann

Reputation: 810

After the accepted answer from Guillermo was written, the CSS transition specification of 2012-04-03 changed the behavior of the visibility transition and now it is possible to solve this problem in a shorter way, without the use of transition-delay:

.myclass > div {
                   transition:visibility 1s, opacity 1s;
                   visibility:hidden;  opacity:0
               }
.myclass:hover > div
               {   visibility:visible; opacity:1 }

The run time specified for both transitions should usually be identical (although a slightly longer time for visibility is not a problem).

For a running version, see my blog post CSS Transition Visibility.

W.r.t. the title of the question "Transitions on the display: property" and in response to comments from Rui Marques and josh to the accepted answer:

This solution works in cases where it is irrelevant if the display or visibility property is used (as it probably was the case in this question).

It will not completely remove the element as display:none, just make it invisible, but it still stays in the document flow and influences the position of the following elements.

Transitions that completely remove the element similar to display:none can be done using height (as indicated by other answers and comments), max-height, or margin-top/bottom, but also see How can I transition height: 0; to height: auto; using CSS? and my blog post Workarounds for CSS Transitions on the Display and Height Properties.

In response to comment from GeorgeMillo: Both properties and both transitions are needed: The opacity property is used to create a fade-in and fade-out animation and the visibility property to avoid the element still reacting on mouse events. Transitions are needed on opacity for the visual effect and on visibility to delay hiding until the fade-out is finished.

Upvotes: 5

Manish Pradhan
Manish Pradhan

Reputation: 1188

You can add a custom animation to the block property now.

@keyframes showNav {
  from {opacity: 0;}
  to {opacity: 1;}
}
.subnav-is-opened .main-nav__secondary-nav {
  display: block;
  animation: showNav 250ms ease-in-out both;
}

Demo

In this demo the sub-menu changes from display:none to display:block and still manages to fade.

Upvotes: 40

mindfullsilence
mindfullsilence

Reputation: 452

JavaScript is not required, and no outrageously huge max-height is needed. Instead, set your max-height on your text elements, and use a font relative unit such as rem or em. This way, you can set a maximum height larger than your container, while avoiding a delay or "popping" when the menu closes:

HTML

<nav>
  <input type="checkbox" />
  <ul>
    <li>Link 1</li>
    <li>Link 1</li>
    <li>Link 1</li>
    <li>Link 1</li>
  </ul>
</nav>

CSS

nav input + ul li { // Notice I set max-height on li, not ul
   max-height: 0;
}

nav input:checked + ul li {
   max-height: 3rem; // A little bigger to allow for text-wrapping - but not outrageous
}

See an example here: http://codepen.io/mindfullsilence/pen/DtzjE

Upvotes: 5

Edyn
Edyn

Reputation: 2497

Taking from a few of these answers and some suggestions elsewhere, the following works great for hover menus (I'm using this with Bootstrap 3, specifically):

nav .dropdown-menu {
    display: block;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    transition: max-height 500ms, opacity 300ms;
    -webkit-transition: max-height 500ms, opacity 300ms;
}
nav .dropdown:hover .dropdown-menu {
    max-height: 500px;
    opacity: 1;
    transition: max-height 0, opacity 300ms;
    -webkit-transition: max-height 0, opacity 300ms;
}

You could also use height in place of max-height if you specify both values since height:auto is not allowed with transitions. The hover value of max-height needs to be greater than the height of the menu can possibly be.

Upvotes: 11

user2615031
user2615031

Reputation:

You can also use this:

.dropdown {
    height: 0px;
    width: 0px;
    opacity: .0;
    color: white;
}
.dropdown:hover {
    height: 20px;
    width: 50px;
    opacity: 1;
    transition: opacity 200ms;
    /* Safari */
    -webkit-transition: opacity 200ms;
}

Upvotes: 0

foxdanni
foxdanni

Reputation: 301

You can simply use the CSS visibility: hidden/visible instead of display : none/block

div {
    visibility:hidden;
    -webkit-transition: opacity 1s ease-out;
    -moz-transition: opacity 1s ease-out;
    -o-transition: opacity 1s ease-out;
    transition: opacity 1s ease-out;
    opacity: 0;
}

parent:hover > div {
    opacity: 1;
    visibility: visible;
}

Upvotes: 0

mojuba
mojuba

Reputation: 12227

The simplest universal solution to the problem is: feel free to specify display:none in your CSS, however you will have change it to block (or whatever else) using JavaScript, and then you'll also have to add a class to your element in question that actually does the transition with setTimeout(). That's all.

I.e.:

<style>
    #el {
        display: none;
        opacity: 0;
    }
    #el.auto-fade-in {
        opacity: 1;
        transition: all 1s ease-out; /* Future, future, please come sooner! */
        -webkit-transition: all 1s ease-out;
        -moz-transition: all 1s ease-out;
        -o-transition: all 1s ease-out;
    }
</style>

<div id=el>Well, well, well</div>

<script>
    var el = document.getElementById('el');
    el.style.display = 'block';
    setTimeout(function () { el.className = 'auto-fade-in' }, 0);
</script>

This was tested in the latest sane browsers. Obviously it shouldn't work in Internet Explorer 9 or earlier.

Upvotes: 3

Larry Gerndt
Larry Gerndt

Reputation: 1925

I suspect anyone just starting CSS transitions quickly discovers that they don't work if you're modifying the display property (block/none) at the same time. One workaround that hasn't yet been mentioned is that you can continue to use display:block/none to hide/show the element, but set its opacity to 0 so that even when it's display:block, it's still invisible.

Then to fade it in, add another CSS class such as "on" which sets the opacity to 1 and defines the transition for opacity. As you may have imagined, you'll have to use JavaScript to add that "on" class to the element, but at least you're still using CSS for the actual transition.

P.S. If you find yourself in a situation where you need to do both display:block, and add class "on", at the same time, defer the latter using setTimeout. Otherwise, the browser just sees both things as happening at once and disables the transition.

Upvotes: 4

Celmaun
Celmaun

Reputation: 24762

At the time of this post all major browsers disable CSS transitions if you try to change the display property, but CSS animations still work fine so we can use them as a workaround.

Example Code (you can apply it to your menu accordingly) Demo:

Add the following CSS to your stylesheet:

@-webkit-keyframes fadeIn {
    from { opacity: 0; }
      to { opacity: 1; }
}
@keyframes fadeIn {
    from { opacity: 0; }
      to { opacity: 1; }
}

Then apply the fadeIn animation to the child on parent hover (and of course set display: block):

.parent:hover .child {
    display: block;
    -webkit-animation: fadeIn 1s;
    animation: fadeIn 1s;
}

Update 2019 - Method that also supports fading out:

(Some JavaScript code is required)

// We need to keep track of faded in elements so we can apply fade out later in CSS
document.addEventListener('animationstart', function (e) {
  if (e.animationName === 'fade-in') {
      e.target.classList.add('did-fade-in');
  }
});

document.addEventListener('animationend', function (e) {
  if (e.animationName === 'fade-out') {
      e.target.classList.remove('did-fade-in');
   }
});
div {
    border: 5px solid;
    padding: 10px;
}

div:hover {
    border-color: red;
}

.parent .child {
  display: none;
}

.parent:hover .child {
  display: block;
  animation: fade-in 1s;
}

.parent:not(:hover) .child.did-fade-in {
  display: block;
  animation: fade-out 1s;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
<div class="parent">
    Parent
    <div class="child">
        Child
    </div>
</div>

Upvotes: 384

robocat
robocat

Reputation: 5433

display is not one of the properties that transition works upon.

See Animatable CSS properties for the list of CSS properties that can have transitions applied to them. See CSS Values and Units Module Level 4, Combining Values: Interpolation, Addition, and Accumulation for how they are interpolated.

Up to CSS 3 was listed in 9.1. Properties from CSS (just close the warning popup)

I've also tried using height, but that just failed miserably.

Last time I had to do this, I used max-height instead, which is an animatable property (although it was a bit of a hack, it did work), but beware that it may be very janky for complex pages or users with low-end mobile devices.

Upvotes: 61

Related Questions