Search code examples
cssreactjsnext.jstailwind-csscss-position

Fit Next/Image component inside div whilst maintaining aspect ratio and border rounding


I've been trying to style a NextJS image with rounded corners whilst also growing to fit it's containing div (Blue on image, difficult to see but there) and maintaining aspect ratio (unknown until runtime). All that is broken with what I currently have, is the image not getting the border-radius but the box surrounding it does (Black on image). I cannot find a way to get the border radius to work without hard coding the image size, as that must be dynamic. The only other vector to consider is that this is all contained inside another fixed positioned div (Red on image) that contains the whole popup.

Demo image

I have tried the below from sugguestions in other threads and it almost works, the only issue I have found is that the image does not recieve the rounded corners due to it's box being larger than the content and thus rounding that box and not the image.

{/* Card that shows on click */}
            <div className='fixed z-10 w-full h-full left-0 top-0 invisible bg-black/[.6]' id={'hidden-card-' + title} onClick={hideEnlargement}>
                <div className='w-[80%] h-[80%] translate-x-[calc(50vw-50%)] translate-y-[calc(50vh-60%)] rounded-2xl'>
                    <Image
                        src={img} 
                        alt={alt}
                        quality={100}
                        className="rounded-2xl bg-black m-auto"
                        fill={true}
                        style={{"objectFit" : "contain"}}
                    />

                </div>
                
            </div>

Solution

  • A couple of tweaks will get this to work:

    1. I just replaced w-full h-full left-0 top-0 with inset-0 for brevity. The inset-0 class is the same as setting top, right, bottom, left all to 0.
    2. On the wrapper div for your image, round the corners there and set overflow to hidden. Also, since you have m-auto, which will center the image horizontally by distributing the margins evenly left and right, you should remove the translate-x class. I also used the built in h-3/4 and w-3/4 which is 75% width and height.
    3. Finally, set your image to have a height and width of 100% and an object-fit of cover. When using object-fit, cover fills the entire space potentially cropping the top/bottom or left/right of the image. The object-fill property will cause the image to only fill the max height or width of the container so that none of the image is cropped.
    <div
      className="fixed inset-0 z-10 bg-black/[.6]"
      id={'hidden-card-' + title}
      onClick={hideEnlargement}
    >
      <div className="rounded-2xl overflow-hidden h-3/4 w-3/4 m-auto translate-y-[calc(50vh-50%)]">
      <Image
        src={img}
        alt={alt}
        quality={100}
        className="bg-black w-full h-full object-cover"
        fill={true}
        />
      </div>
    </div>
    

    Stackblitz Demo

    Edit

    To avoid cropping but still have the images appear with rounded corners and preserve their intrinsic aspect-ratio, you have to pass the width and height of the images to the Next/Image component. I can't think of a way to do it without because if you don't pass the width and height to the Next/Image component, it requires that you set the fill prop to true.

    When the fill prop is set to true it takes the image out of the document flow by setting it to be absolutely positioned. As a result, you lose the ability to do something like set the width of the image to 100% and the height to auto and just allow the browser to determine the appropriate dimensions based on the natural width/height of the image.

    So if you store the natural/desired height and width of the images with the image details, you can then avoid using the fill prop.

    Your modal could then look like this:

    // assumes you pass props with an img object containing
    // src, alt, width and height properties
    <div
      className="fixed inset-0 z-10 bg-black/[.6] grid place-content-center"
      onClick={closeModal}
    >
      <Image
        src={img.src}
        alt={img.alt}
        width={img.width}
        height={img.height}
        quality={100}
        className="max-w-[95vw] max-h-[95vh] rounded-2xl animate-bounceIn"
      />
    </div>
    
    

    You can see that since the image has height and width, you can use grid place-content-center to center everything without any transform or margins required. Also, you don't need the extra wrapping div. The image itself will be its natural height and width, but to make sure that it doesn't overflow the viewport, you can set the max-width and max-height of the image. Of course, this is where the problem comes in with the width and height being set on the image. Once the max-height or max-width is applied, it causes the image to be out of proportion. Setting object-fit to contain works, but you'll lose the rounded corners.

    You could instead optimize your images manually and just use an img tag or picture tag.

    New Stackblitz Demo

    In the new Stackblitz demo there are multiple images of varying heights and widths. You can see the structure of the data in the pages/api/images.js file. The new "modal" component can be found in the `pages/components/Modal.js file.