22

I've got a div element that maintains an aspect ratio: it calculates its height based on its width (using the padding trick). What I'd like to do is to put this div into another one by fitting the maximum space available, vertically and horizontally, no crop. I think the closest thing to what I want is the object-fit: contain - which is img only.

I want the div to cover the max height and width possible while maintaining the aspect ratio. No vertical or horizontal crop.

Is it even possible with CSS only? If so, how?

Update: A good article where things are at the moment.

code (Can be any other solution, doesn't have to be built on this snippet):

html,
body {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.container {
  position: relative;
  width: 100%;
}

.container:before {
  content: "";
  display: block;
  width: 50%;
  padding-top: 50%;
}

.embed {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: red;
}
<div class="container">
  <div class="embed">
    this should accommodate all the available space and maintain aspect ratio, no crop when width is too wide
  </div>
</div>

enter image description here

haxpanel
  • 4,402
  • 4
  • 43
  • 71

5 Answers5

10

aspect-ratio with overflow:hidden on Chromium 88, Firefox 87 , and Safari Technology Preview 118 is your friend.

Update January 2023:

Now you'll need a @container (aspect-ratio > X) container query too. Firefox v110.0b2, Safari and Chromium all pass.

I updated the code below, also the demo on jsitor.

html,
body {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.container {
  container-type: size;
  container-name: resize-box;
  display: grid;
  resize: both;
  overflow: hidden;
  border: black 2px solid;
  min-width: 50px;
  min-height: 50px;
  width: 150px;
}

.embed {
  width: 100%;
  aspect-ratio: 1/1;
  object-fit: contain;
  overflow: hidden;
  border: 2px red solid;
  box-sizing: border-box;
  display: flex;
  margin: auto;
}

@container resize-box (aspect-ratio > 1/1) {
  .embed {
    width: auto;
    height: 100%;
  }
}

.embed > div {
  margin: auto;
}
<div class="container">
  <div class="embed">
    <div>1:1</div>
  </div>
</div>
Semmel
  • 1,040
  • 10
  • 14
4

Ok, it looks like it can't be solved by CSS only. If anyone interested, I've put together a React component that does the job (Tests and better README soon, when I have time).

It wraps its children into a div and uses JavaScript to compute the width and height of that div in order to accommodate the available space while maintains the given aspect ratio. It basically stretches the wrapper until one of the sides reaches its maximum.

BREAKING UPDATE a CSS only solution has been found!

haxpanel
  • 4,402
  • 4
  • 43
  • 71
  • 1
    A link to a solution is welcome, but please ensure your answer is useful without it: [add context around the link](//meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted.](//stackoverflow.com/help/deleted-answers) – dippas May 12 '18 at 17:43
  • 5
    Your "BREAKING UPDATE" is based on the viewport width/height, not that of the parent element. – David Callanan Oct 26 '20 at 10:57
4

The only workaround I managed to achieve so far is to wrap the child element in to svg's foraignObject tag:

const container = document.getElementById('container');
document.getElementById('btn').addEventListener('click', () => {
  container.style.height = container.style.height === '100px' ? '200px' : '100px';
});
body {
  margin: 1rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

button {
  margin-bottom: 1rem;
}


#container {
  background-color: #ffceaf;
  width: 400px;
}

svg {
  background-color: #b8d6ff;
  height: auto;
  width: auto;
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
}

#content {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border: solid;
}
<button id="btn">Change parent height</button>

<div id="container" style="height: 100px;">
  <svg width="15000" height="5000">
    <foreignObject width="100%" height="100%">
      <div id="content">
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
        content content content content content content content
      </div>
    </foreignObject>
  </svg>
</div>

This solution has downsides:

  • Browser compatibility (IE/Edge do not support foraignObject properly)
  • It's not a best practice. Including elements from a different XML namespace may cause futher issues(?).

I feel like using JS might be better option here.

Tim
  • 119
  • 3
2

Here's my CSS-only solution:

Codepen Link or run demo here ⬇

.resizable-container {
  resize: both;
  overflow: scroll;
  border: 1px solid black;
  padding: 3px;
  height: 140px;
  width: 330px;
}

.container {
  height: 100%;
  box-sizing: border-box;
}

.aspect-thing {
  aspect-ratio: 16/9;
  border: 2px red solid;
  box-sizing: border-box;
  
  max-width: 100%;
  max-height: 100%;
  margin: auto;
}
<div class="resizable-container"> 
  <div class="container">
    <div class="aspect-thing">
    </div>
  </div>
</div> 
<p>Resize me ^</p>

enter image description here

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Ben Winding
  • 10,208
  • 4
  • 80
  • 67
1

I'm not sure if this is the best solution, but it is pure css. It uses nested flexboxes. The outer is flex-direction: row and the inner is flex-direction: column. It is important the inner also has to have aspect-ratio is set to 1/1 and notice that inner's height is set and inner:before's width is set.

<div class="outer">
  <div class="inner"></div>
</div>
.outer {
  width: 80%;
  height: 80%;
  margin: auto;
  background-color: red;
  padding: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.inner {
  height: 100%;
  aspect-ratio: 1 / 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}

.inner:before {
  content: '';
  width: 100%;
  aspect-ratio: 1 / 1;
  background-color: blue;
}
szodi
  • 11
  • 3