Keep it contained!

Enforcing an aspect ratio on an HTML element in React and CSS

Jesse Pinho
Bleeding Edge
4 min readAug 9, 2018

--

Illustration by Marta Pucci

Frequently, it’s necessary to set an aspect ratio on an element (like a <div> element), so that it will maintain its shape while scaling to any size. For example, you may be displaying a YouTube video in an iframe, and you want to make sure it shows up in 16:9.

This is relatively simple to accomplish due to a CSS quirk. Per w3.org, when you set the CSS padding for a box:

The percentage is calculated with respect to the width of the generated box’s containing block, even for ‘padding-top’ and ‘padding-bottom’.

That is, if you have a 200 pixel-wide element and you set its padding-bottom to 50%, the bottom padding will be 100 pixels—regardless of the element’s height!¹

We can take advantage of this fact to set an aspect ratio for an element, even as the window scales to any size. All we have to do is divide the height of our aspect ratio by its width, and then use that as the percentage for padding-bottom. For example, to enforce a 16:9 aspect ratio for an element, divide 9 by 16, which is 0.5625. Then, set the padding-bottom to 56.25%. Note that you also have to set the height to 0 to ensure that the padding constitutes the entirety of the element. Here’s a full CSS class for a 16:9 container:

.aspect-ratio--16x9 {
width: 100%;
height: 0;
padding-bottom: 56.25%;
}

Working out the quirks

One issue you might face here is that, while the 16:9 container takes up the right amount of space on the page, its height is set to 0. While this will work fine for displaying a background image cropped to a specific aspect ratio, it doesn’t handle actual content well. Take the YouTube iframe from earlier: if you put an iframe into this container, it won’t actually fit into the aspect ratio you’ve specified. By default, YouTube’s embed code includes width and height attributes on the video player iframe, so it will stick out of your container element if it’s too big.

Meanwhile, if you style it with CSS to have a width and height of 100%, it will disappear! This is because, when you set its height to 100%, it will match the height of its container, which is 0. So, how do you get the iframe to match the aspect ratio of the container element?

The solution is to add an inner wrapper to the container element, which stretches to fill it. We can do that by absolute-positioning it:

.aspect-ratio__inner-wrapper {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

We then have to add position: relative to the container, so that it positions its inner wrapper correctly:

.aspect-ratio--16x9 {
width: 100%;
height: 0;
padding-bottom: 56.25%;
position: relative;
}

Then, we can safely give the iframe a width and height of 100%, and it will maintain its aspect ratio at all screen sizes! Here’s the complete code example:

/* styles.css */.aspect-ratio--16x9 {
width: 100%;
height: 0;
padding-bottom: 56.25%;
position: relative;
}
.aspect-ratio__inner-wrapper {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.youtube-video {
width: 100%;
height: 100%;
}
<!-- page.html --><div class="aspect-ratio--16x9">
<div class="aspect-ratio__inner-wrapper">
<iframe class="youtube-video" src="https://youtube.com/..."></iframe>
</div>
</div>

Doing it the React way

Since I need to solve this problem frequently on Clue’s website, I didn’t want to have to create two <div>s and assign their classes manually each time I needed to enforce an aspect ratio on an element. Since we use styled-components as our CSS-in-JS solution, I was able to easily create dynamic CSS classes containing the desired aspect ratio. So I decided to create an <AspectRatio /> component to solve this for me:

// AspectRatio.tsximport * as React from "react"
import * as Styles from "./AspectRatio.styles"
interface Props {
children?: any
/**
* The width divided by the height. This ratio can be passed in
* using JavaScript division syntax. So, to get a 16:9 ratio,
* simply pass `ratio={16/9}`.
*/
ratio: number
}
const AspectRatio = ({ children, ratio }: Props) => (
<Styles.OuterWrapper ratio={ratio}>
<Styles.InnerWrapper>
{children}
</Styles.InnerWrapper>
</Styles.OuterWrapper>
)
export default AspectRatio
// AspectRatio.styles.tsimport styled, { StyledFunction } from "styled-components"const outerWrapper: StyledFunction<{ ratio: number}> = styled.div
export const OuterWrapper = outerWrapper`
position: relative;
width: 100%;
height: 0;
/**
* For human readability, the ratio is expressed as
* width / height, so we need to invert it.
*/
padding-bottom: ${props => (1 / props.ratio) * 100}%;
`
export const InnerWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`

This component abstracts away all the complexity of the CSS classes we discussed earlier. It also exposes a nice-to-use prop, ratio, which can be passed in as a simple JavaScript division expression—e.g., <AspectRatio ratio={16 / 9} />.

Let’s demonstrate with an example. Going back to the YouTube example, let’s create a <YouTube /> component that renders videos with a 16:9 aspect ratio:

// YouTube.tsximport * as React from "react"
import styled from "styled-components"
import AspectRatio from "./AspectRatio"
const ResponsiveIframe = styled.iframe`
width: 100%;
height: 100%;
`
interface Props {
id: string
}
const YouTube = ({ id }: Props) => (
<AspectRatio ratio={16 / 9}>
<ResponsiveIframe src={`https://www.youtube.com/embed/${id}`} />
</AspectRatio>
)
export default YouTube

There you have it! A YouTube video that will scale to any size while maintaining a 16:9 aspect ratio.

¹ Thanks to this StackOverflow post for finding the W3 spec on using percentages in padding.

--

--

Jesse Pinho
Bleeding Edge

Making things with code at Clue. Trying to figure out how to human better.