Search code examples
htmlcssflexbox

Is there something like `object-fit: contain` for non-replaced elements?


Recently, I've been trying to create a centered div with an aspect ratio of 3 / 7 that fills its parent the best can, acting something like object-fit: contain. Unfortunately, object-fit: contain doesn't seem to work for non-replaced elements.

Here's a visualization of what I want to achieve in Flutter, which is something I have slightly more experience in.

https://dartpad.dev/?id=eec1210050a099df1c5422fa66e891e5

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: AspectRatio(
              aspectRatio: 3 / 7,
              child: ColoredBox(color: const Color(0xFF42A5F5))),
        ),
      ),
    );
  }
}

When when the height of the container is less than its width, the box's height should be the same as its container.

enter image description here

When when the width of the container is less than its height, the box's width should be the same as its container.

enter image description here

All the while, it should always stay in the center of the screen and maintain its 7 / 3 aspect ratio.

This is what I've come up with so far, but it doesn't react to the height becoming less than the width.

* {
  margin: 0;
}

html,
body {
  width: 100%;
  height: 100%;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  flex-direction: column;
}

.blue {
  background-color: royalblue;
  aspect-ratio: 3 / 7;
  flex-grow: 1;
  max-width: 100%;
}
<div class="container">
  <div class="blue">
  </div>
</div>

codepen

On Discord, just_13eck suggested a solution that had this CSS instead:

body {
  margin: 0;
}

.container {
  display: flex;
  block-size: 100vh;
}

.blue {
  flex: 1 1 0;
  background-color: royalblue;
  aspect-ratio: 3 / 7;
  margin-inline: auto;
  margin-block: auto;
  max-inline-size: calc((3 / 7) * 100vh);
  max-block-size: 100%;
}

And it works! BUT it only works relative to the viewport size.

Could anybody help me? I'd really appreciate it; Thanks!


Solution

  • NEW ANSWER

    Thanks to @C3Roe for the container query idea!

    * {
      margin: 0;
    }
    
    html,
    body {
      width: 100%;
      height: 100%;
    }
    
    .containment-area {
      width: 100%;
      height: 100%;
      container: containment / size;
    }
    
    .container {
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
    }
    
    @container containment (aspect-ratio < 3 / 7) {
      .container {
        flex-direction: row;
      }
    }
    
    .non-replaced-element {
      background-color: royalblue;
      aspect-ratio: 3 / 7;
      flex-grow: 1;
    }
    <div class="containment-area">
      <div class="container">
        <div class="non-replaced-element">
        </div>
      </div>
    </div>

    codepen

    With help from this StackOverflow answer.

    OLD ANSWER

    "You're not going to be able to remove the viewport unit dependencies. By default, block-level elements take up only as much vertical room as the content requires. In this case, there is no content so the elements shrink to literally nothingness. If you want there to be content-less height you use vh. Or even if you want there to be contentful height, you use vh to set that initial full-height containing block." - just_13eck

    If you're willing to use JS though, you can use this solution:

    function resizeInner() {
      const container = document.querySelector('.container');
      const inner = document.querySelector('.inner');
      const aspectRatio = 3 / 7; // width / height
      // Get the computed style to accurately get the container's dimensions
      const containerStyle = window.getComputedStyle(container);
      const containerWidth = parseFloat(containerStyle.width);
      const containerHeight = parseFloat(containerStyle.height);
      let innerWidth, innerHeight;
      if (containerWidth / containerHeight > aspectRatio) {
        // Container is wider than the target aspect ratio
        innerHeight = containerHeight;
        innerWidth = innerHeight * aspectRatio;
      } else {
        // Container is taller than the target aspect ratio
        innerWidth = containerWidth;
        innerHeight = innerWidth / aspectRatio;
      }
      inner.style.width = innerWidth + 'px';
      inner.style.height = innerHeight +'px';
    }
    // Initial resize
    resizeInner();
    // Resize on window resize
    window.addEventListener('resize', resizeInner);
    // If the container size might change due to other factors, you can use a ResizeObserver
    const resizeObserver = new ResizeObserver(resizeInner);
    resizeObserver.observe(document.querySelector('.container'));
    * {
      margin: 0;
    }
    
    html, body {
      width: 100%;
      height: 100%;
    }
    
    .container {
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      background-color: #f0f0f0;
    }
    
    .inner {
      background-color: #3498db;
    }
    <div class="container">
      <div class="inner">
        
      </div>
    </div>

    Adapted from this codepen of West-Ad7482 on Reddit.