Responsive Sizing Without Using Absolute Positioning

Responsive Sizing Without Using Absolute Positioning

Background

Often times you want to size some DOM element based on an aspect ratio. This is useful for images and video, where you want the media to fill the available space while maintaining the original aspect ratio. The classic solution to this is outlined here: https://css-tricks.com/aspect-ratio-boxes, and is sometimes referred to as the padding bottom (or top) trick.

One drawback of this approach is that it requires you to place your content within a position: absolute container. This means that anything inside that may have position: absolute is now positioned relative to the container instead of what it otherwise would. The following contrived example shows where this limitation could cause problems:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Using padding-top + position: absolute; to achieve a 16:9 aspect ratio, with scrolling content inside. We do not want the orange box to scroll with the content.

In this case, you could move the orange box outside of the scrolling area, but there may be cases where your DOM structure does not allow you to do this.

tl;dr

See the CSS and markup to achieve the layout.

Sizer With position: absolute

First, lets take a look at the classic padding trick. Essentially, we have some element (a real or pseudo element) using padding-top to make our container the desired size. We then use position: absolute to make our content area match the size of the container.

.container {
  position: relative;
  overflow: hidden;
}

.content-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
<div class="container" style="width: 300px;">
  <div class="sizer" style="padding-top: 56.25%"></div>
  <div class="content-fill" style="background-color: #26e;">…</div>
</div>
A div rendered with 16:9 aspect ratio using a sizer element, using position absolute to fill the container.

Sizer With display: table

Where before we used position: absolute to match sizes, this approach uses table layout's behavior. An element with display: table will divide the available space for all columns evenly. Keeping the same structure as before, lets instead use the table display and see what we get:

.container {
  display: table;
  overflow: hidden;
}

.container > .content-fill,
.container > .sizer {
  display: table-cell;
}
Using a container with display: table, with one column for the sizer and one for the content.

Note that our content area takes up half the desired space. While our sizer is giving our container the correct height, we are still sharing the overall width with the sizer element. We need to now make sure that we give all the available space to the content. Two approaches are:

  • Give the container table-layout: fixed and the sizer width: 0
  • Give the content width: 200%

Both work, but I have gone for the former approach as it is a bit more clear.

.container {
  …
  table-layout: fixed;
}

.container > .sizer {
  width: 0;
}

Now we have something that looks like it is the correct size:

Using table layout to size the content, while using all the width for the content and none for the sizer.

While it looks good so far, we are not quite done yet. The content area may contain content that causes overflow. If we simply put the content inside at this point, it will cause our table-cell's height to grow. We need to make sure that our content is limited to the size of the cell. Since cells always grow their height to fit the content, we need to create an inner container that is limited by the size of the cell:

.container > .content-fill {
  height: 1px;  
}

.container .content-fill-inner {
  height: 100%;
}
<div class="container" style="width: 300px;">
  <div class="sizer" style="padding-top: 56.25%"></div>
  <div class="content-fill">
    <div class="content-fill-inner" style="background-color: #26e;">
      …
    </div>
  </div>
</div>

Note that we gave the table-cell an explicit height (the value is not important). This is needed so that our height: 100% for the inner container is used.

Now we have a container, we can put some content inside that will respect our aspect ratio. We can also use position: absolute to position to our original container rather than the scrolling area. For example:

<div class="container" style="width: 300px; position: relative;">
  <div class="sizer" style="padding-top: 56.25%"></div>
  <div class="content-fill">
    <div class="content-fill-inner" style="background-color: #26e; overflow-y: auto; …" >
      …
      <div style="position: absolute; bottom: 6px; right: 6px; …"></div>
    </div>
  </div>
</div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Using overflowing content within the content area, with an element inside the scrolling area positioning relative to the outer container.

The Complete Code

.container {
  display: table;
  table-layout: fixed;
  overflow: hidden;
}

.container > .content-fill,
.container > .sizer {
  display: table-cell;
}

.container > .sizer {
  width: 0;
}

.container > .content-fill {
  height: 1px;  
}

.container .content-fill-inner {
  height: 100%;
}
<div class="container">
  <div class="sizer" style="padding-top: 56.25%"></div>
  <div class="content-fill-sizer">
    <div class="content-fill-inner">
      <!-- Content goes here -->
    </div>
  </div>
</div>