Keith
Keith

Reputation: 155872

Why is a margin-top percentage using height and how can I work around it?

I have a CSS slide down menu that can vary in the number of elements displayed.

I use an overflow: hidden wrapper and margin-top: -100% to hide the menu, and a CSS transition with margin-top: 0 to animate the slide down.

The problem appears to be that margin-top: -100% seems to be picking up the explicit width of any parent element and ignoring the content height.

.outer {
  overflow: hidden;
  border: 10px solid red;
  cursor: pointer;
}
.outer > .slide {
  margin-top: -100%;
  transition-property: margin-top;
  transition-duration: 1s;
}
.outer:hover > .slide {
  margin-top: 0;
}
.outer.bug {
  width: 100px;
}
<div class="outer">
  <div class="slide">
    <ul>
      <li>
        Ayy
      </li>
      <li>
        Bee
      </li>
      <li>
        See
      </li>
      <li>
        Dee
      </li>
      <li>
        Eee
      </li>
      <li>
        Eff
      </li>
      <li>
        Gee
      </li>
    </ul>
  </div>
</div>

<div class="outer bug">
  <div class="slide">
    <ul>
      <li>
        Ayy
      </li>
      <li>
        Bee
      </li>
      <li>
        See
      </li>
      <li>
        Dee
      </li>
      <li>
        Eee
      </li>
      <li>
        Eff
      </li>
      <li>
        Gee
      </li>
    </ul>
  </div>
</div>

Run that snippet and you should see two red boxes, mouse over either to get the slide down menu.

The problem is the second box - the only difference is that the parent has width: 100px; set.

If you inspect the second box you should see that the margin-top: -100%; has been calculated as -100px.

In fact any explicit or inherited width against the parent div will become the negative margin.

The only way I've found round it is to explicitly set the margin-top to be the same as the height of the sliding menu, but that involves DOM layout and JS that I don't really want to do for each menu.

Is this a bug in how browsers handle negative percentages on margin-top? IE11, Chrome and FX all seem to treat it the same way.

Is there a better way around it that doesn't require an explicit height calculation and additional Javascript?

Upvotes: 2

Views: 3244

Answers (2)

frnt
frnt

Reputation: 8795

margin-top:-100% works only when we remove border of 20px assigned .outer which is used twice 10px + 10px. So we can make use of CSS calc() function over-here to get our output.

ul has a default padding-top:40px so this too is used twice 40px + 40px, which we have to set to 0px.

So ul and border is the reason for not working of margin-top:-100%;

Default ul properties :

ul{
display: block;
list-style-type: disc;
margin-before: 1em;
margin-after: 1em;
margin-start: 0;
margin-end: 0;
padding-start: 40px;
}

.outer {
  overflow: hidden;
  border: 10px solid red;
  cursor: pointer;
  width:auto;
  height:auto;
}
.outer > .slide {
  margin-top: calc(-26px + (-100%));
  transition-property: margin-top;
  transition-duration: 1s;
  background:#ccc;
}
.outer > .slide > ul{
  padding:0;
  margin:0;
}
.outer:hover > .slide {
  margin-top: 0;
}
.outer.bug {
  width: 100px;
}
<div class="outer">
  <div class="slide">
    <ul>
      <li>Ayy</li>
      <li>Bee</li>
      <li>See</li>
      <li>Dee</li>
      <li>Eee</li>
      <li>Eff</li>
      <li>Gee</li>
    </ul>
  </div>
</div>

<div class="outer bug">
  <div class="slide">
    <ul>
      <li>Ayy</li>
      <li>Bee</li>
      <li>See</li>
      <li>Dee</li>
      <li>Eee</li>
      <li>Eff</li>
      <li>Gee</li>
    </ul>
  </div>
</div>

Upvotes: 1

Paulie_D
Paulie_D

Reputation: 115342

To move an element (regardless of it's height) in relation to its own dimensions you can use a transform:translate() instead of margin.

So your requirement to move the element up by its own height would require a translation in the Y-axis of -100%. Like so:

* {
  margin: 0;
  padding: 0;
}
.outer {
  /* overflow: hidden; */
  padding: 10px;
  background: red;
  cursor: pointer;
  position: relative;
}
.outer > .slide {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(-100%);
  transition-property: transform;
  transition-duration: 1s;
}
.outer:hover > .slide {
  transform: translateY(0%);
}
<div class="outer">
  <div class="slide">
    <ul>
      <li>Ayy</li>
      <li>Bee</li>
      <li>See</li>
      <li>Dee</li>
      <li>Eee</li>
    </ul>
  </div>
</div>

Then we can use absolute positioning to remove the child from the document flow.

Upvotes: 1

Related Questions