How to make div element auto-resize maintaining aspect ratio?

I'm trying to have a div centered in the screen. This div should have a specific width and height when it fits in the available window space, but it should shrink to fit when the available window space is not enough, but also, maintaining its original aspect ratio.

I've been looking at lots of examples that work for a decreasing width, but none that work for both width and height changes in the window size.

Here's my current CSS:

* {
    border: 0;
    padding: 0;
    margin: 0;
}
.stage_wrapper {
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: gray;
}
.stage {
    width: 960px;
    height: 540px;
    max-width: 90%;
    max-height: 90%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: chocolate;
    object-fit: contain; /* I know this is for images, it's an example of what I'm looking for */
}

And, my current HTML:

<div class="stage_wrapper">
    <div class="stage">
        <p>Some text</p>
    </div>
</div>

This is showing a centered div that has a fixed width of 960px and a fixed height of 540px. It should never be bigger than that.

Then, if I change the size of my window to have a smaller width or height than that, the div element is successfuly shrinking - except it is not maintaining the original aspect ratio, and that is what I'm looking for. I want it to respond to changes in both width and height.

Is this possible at all?


Here is an idea using viewport unit and clamp(). It's a kind of if else to test if the width of the screen is bigger or smaller than the height (considering the ratio) and based on the result we do the calculation.

In the code below with have two variables cv and ch and only one of them will be equal to 1

  • If it's cv then the width is bigger so we set the height to cv and the width will be based on that height so logically cv/ratio

  • If it's ch then the height is bigger so we set the width to cv and the height will be based on that width so logically ch/ratio

In the clamp() I am using 1vh/1vw that I multiple by 90 which is equivalent to your 90% to have 90vh/90vw

body {
  height: 100vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background: gray;
}


.stage {
  --r: calc(960 / 540);
  
  --cv: clamp(0px,(100vw - 100vh*var(--r))*10000,1vh);
  --ch: clamp(0px,(100vh*var(--r) - 100vw)*10000,1vw);

  height: calc((var(--cv) + var(--ch)/var(--r)) * 90 );
  width:  calc((var(--ch) + var(--cv)*var(--r)) * 90 );
  
  max-width: 960px;
  max-height: 540px; /* OR calc(960px/var(--r)) */
  
  display: flex;
  justify-content: center;
  align-items: center;
  
  
  background: 
    /* this gradient is a proof that the ratio is maintained since the angle is fixed */
    linear-gradient(30deg,red 50%,transparent 50%),
    chocolate;
}
<div class="stage">
  <p>Some text</p>
</div>

In theory the clamp can be simplified to:

--cv: clamp(0px,(1vw - 1vh*var(--r)),1vh);
--ch: clamp(0px,(1vh*var(--r) - 1vw),1vw);

But to avoid any rounding issue and to not fall into values like 0.x I consider a big value to make sure it will always be clamped to 1 if positive

UPDATE

It seems there is a bug with Firefox so here is another version of the same code:

body {
  height: 100vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background: gray;
}


.stage {
  --r: calc(960 / 540);
  
  --cv: clamp(0px,(100vw - 100vh*var(--r))*100,90vh);
  --ch: clamp(0px,(100vh*var(--r) - 100vw)*100,90vw);

  height: calc((var(--cv) + var(--ch)/var(--r)) );
  width:  calc((var(--ch) + var(--cv)*var(--r)) );
  
  max-width: 960px;
  max-height: 540px; /* OR calc(960px/var(--r)) */
  
  display: flex;
  justify-content: center;
  align-items: center;
  
  
  background: 
    /* this gradient is a proof that the ratio is maintained since the angle is fixed */
    linear-gradient(30deg,red 50%,transparent 50%),
    chocolate;
}
<div class="stage">
  <p>Some text</p>
</div>

Shortly we can simplify all the above and use the aspect-ratio ref property:

body {
  height: 100vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background: gray;
}


.stage {
  --r: 960 / 540;

  aspect-ratio: var(--r);
  width:min(90%, min(960px, 90vh*(var(--r))));
  
  display: flex;
  justify-content: center;
  align-items: center;
  
  
  background: 
    /* this gradient is a proof that the ratio is maintained since the angle is fixed */
    linear-gradient(30deg,red 50%,transparent 50%),
    chocolate;
}
<div class="stage">
  <p>Some text</p>
</div>

Any solution using vw & vh assumes your container is the viewport. But what if you need an element that letterboxes to fit any container?

In other words, you want the behavior of object-fit: contain for an element that's not an <img> or <video>.

You need a container that centers both vertically and horizontally:

#container {
  display: flex;
  align-items: center;
  justify-content: center;
}

and a contained object with fixed aspect-ratio that stretches either its width or height to 100%:

#object {
  aspect-ratio: 16/9;
  overflow: hidden;
  [dimension]: 100%;
}

where [dimension] is width if the container is tall, and height otherwise.

If you don't know ahead of time whether it's going to be tall or not, a little javascript is needed to check for tallness and decide which dimension to stretch.

function update() {
  const isTall = container.clientWidth / container.clientHeight < aspectRatio;
  object.style.width = isTall ? '100%' : 'auto';
  object.style.height = isTall ? 'auto' : '100%';
}

new ResizeObserver(update).observe(container);

I don't think this dimension switching can be accomplished in pure CSS without javascript.

Here's a demo.