Visually slide down fixed element with items aligned at flex-end

802 Views Asked by At

I display an app drawer-like component in my mobile web app where I use bottom navigation icons with more which opens additional options.

Some options-drawer facts

  1. Slides up/down into/out of the view
  2. Has full screen height
  3. Vertically aligns items at the bottom
  4. Displays N options/per row (unlike attached image which only has one option per row)
  5. Is placed within the same position: fixed container at the bottom of the screen as the main bottom navigation.

My options drawer should animate similar to what can be seen on this iOS animated GIF

https://jsfiddle.net/LL4dst15/

App drawer

The problem

My drawer is using flexbox (as you can see from the example) where it aligns elements to flex-end on cross axis to display them at the very bottom. But the problem is the navigation container which is fixed positioned and has such z-index that it gets displayed over the content at all times.

translateY problem

If I use translateY the drawer actually slides in/out as it should, but drawer element's position doesn't change which means that navigation container still has the height of the drawer + bottom bar. This can be seen in my fiddle example on the left where the gray element is always seen. This would therefore cover my actual content so users would have difficulties interacting with it.

I could however use pointer-events: none; but I consider this a rather ugly hack that may have problematic browser support. So I would like to avoid it.

max-height problem

If I use max-height instead of transformations, then navigation container actually does resize when drawer resizes. The problem with this approach is that drawer doesn't seem to slide up/down, but rather folds as blinds... The reason is cross axis alignment to flex end. If I'd align to flex start then it would seem as if it's sliding out.

I was trying to resolve this one with auto margins, but couldn't seem to make it work, so that I would have flex start cross axis alignment but using auto margins to push content to the bottom of the drawer. No luck...

Do you have any other suggestions how should I do CSS so that my drawer would slide and my container would also resize?

3

There are 3 best solutions below

0
On BEST ANSWER

Solution with translateY and delayed max-height

Using animations has the drawback of having the animation of sliding out on load which should be separately handled by javascript, which I don't like. The solution to sliding out with translateY is to also resize it after transform has finished. Maximum height transition can be without animation afterwards, as long as it's done.

So on slide out instead of just doing the

transition: transform $duration; // plus opacity of you want

one should be doing

transition: transform $duration, max-height 0s $duration;

This will complete the Y translation normally and then also change element's height to 0, which will resize container accordingly which was the problem in the first place. Mind that immediate transition of max-height has time dimension 0s instead of just a simple 0 which would be ignored by browser.

If animation is long enough for users to start clicking content while Y translation is in progress one can also set pointer-events: none; to container, but don't forget to set it to auto on child elements because this property is inherited.

That will be the optional solution to the problem.

Here's the resulting JSFiddle with upper implemented.

2
On

You should add the code below to your .extras class after the drawer().

li {
    flex: 1 0 100%;
}

The JSFiddle : https://jsfiddle.net/k9apwnsq/2/

1
On

I tried using css animation and the keyframes; JS Fiddle

There is only one issue remaining: the code also runs when you load the page. This could be easily fixed with some jQuery.

Short version of what I did: I animated the transform: translateY() to get the effect you want, then in the last % of the animation I set your max-height to: 0vh. Personally I would prefer to do this with jQuery, but you wanted a css solution, so here you go.

Here is the code (The css is a bit messy now with all the outcommented things, but cleaning it up is your job) :

html

Click any bottom bar item to toggle options drawer
<nav>
  <ul class="bottom-nav">
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
    <li>Four</li>
  </ul>
  <ul class="extras" label="Uses 'translateY' to slide drawer">
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
  </ul>
</nav>
<nav>
  <ul class="bottom-nav">
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
    <li>Four</li>
  </ul>
  <ul class="extras" label="Uses 'max-height' to slide drawer">
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
    <li>Option</li>
  </ul>
</nav>

css

html, body {
  background-color: #fff;
  line-height: 1.25rem;
  margin: 0;
}
nav {
  position: fixed;
  bottom: 20px;
  left: 10px;
  width: 300px;
  display: flex;
  flex-direction: column;
  background-color: #eee;
  border: 1px solid #fcc;

  ul {
    list-style: none;
    margin: 0;
    padding: 0;
    li {
      margin: 0;
      padding: 0;
    }
  }

  @mixin drawer() {
    width: 100%;
    display: flex;
    flex-flow: row wrap;
    align-content: flex-end;
    align-items: flex-start;
    cursor: default;
    background-color: #fff;
    box-shadow: 0 0 10px #999;
    overflow: hidden;
    &:before {
      content: attr(label);
      position: absolute;
      margin: 5px 0 0 10px;
      color: #999;
      font-size: 0.8125rem;
      font-style: italic;
    }
    li {
      flex-basis: 25%;
      text-align: center;
      padding: 1rem 0;
      &:hover {
        background-color: #f4f4f4;
      }
    }    
  }

  .bottom-nav {
    @include drawer();
    order: 1;
    z-index: 1;
  }

  .extras {
    @include drawer();
    order 0;
    z-index: 0;
    /* 100vh   = full height
     * 3.25rem = bottom nav bar (2×1rem padding + 1.25 line height)
     * 1.25rem = top text line height
     * 20px    = navigation fixed position bottom offset
     * 2px     = navigation container top and bottom border
     */
    height: calc(100vh - 3.25rem - 1.25rem - 20px - 2px);
    /*transform: translateY(100%);*/
    /*transition: transform 1s, opacity 0.5s;*/
    animation: closedrawer 10s forwards;
  }
  &.open {
    .extras {
      /*opacity: 1;*/
      animation: opendrawer 10s 1 forwards;
      /*transform: none;*/
      /*transition: transform 1s, opacity 0.5s 0.25s;*/
    }
  }

  + nav {
    left: 320px;

    .extras {
      /*transform: none;
      max-height: 0;
      transition: max-height 1s, opacity 0.5s 0.25s;*/
    }

    &.open {
      .extras {
        /*max-height: 100vh;
        transition: max-height 1s, opacity 1s;*/
      }
    }
  }
}
@keyframes opendrawer {
    0% {transform: translateY(100%); opacity: 1;}
    99% {background-color: blue; transform: translateY(0%); max-height: 100vh;}
    100% {background-color: blue; transform: translateY(0%);max-height: 100vh;opacity:1;}
}
@keyframes closedrawer {
  0%{transform: translateY(0%)}
   99% {background-color: red; transform: translateY(100%);max-height: 100vh; opacity: 1;}
    100% {background-color: red; transform: translateY(100%); max-height: 0vh; opacity: 0;}
}

javscript

$(function(){
    $(".bottom-nav li").click(function(evt){
    evt.preventDefault();
$(this).closest("nav").toggleClass("open");
  })
})