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 tocv
and the width will be based on that height so logicallycv/ratio
-
If it's
ch
then the height is bigger so we set the width tocv
and the height will be based on that width so logicallych/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.