Trick for Resizable and Responsive CSS Images

Every once in a while in CodePen’s Popular or Picked section, you’ll come across a Pen name containing #dailycssimages. These Pens are vector images created using only HTML elements and CSS. These images usually consist of absolutely positioned elements formed using a variety of properties including border-radius, box-shadow, and sometimes clip-path. For plenty of examples, just do a search for the hashtag. I have been drawing some myself including this Longcat and radar animation. Yet, I’ve always wondered if there was a way to make them resizable or responsive.

I haven’t found too many examples of responsive CSS images in the wild. If responsive somewhat, you’ll find at least a few media queries in the code to define smaller or larger sizes. Just like responsive <img> elements, CSS images made responsive should gradually and proportionally increase or decrease in size if you resize the browser window. Moreover, it would save so much time if we could resize them manually like choosing a new width or height value for a certain <img> element (in CSS that is) and using auto for the other dimension. We may have scale() of the transform property, but the space the image occupies won’t be adjusted along with the image if used.

After endless tinkering, I finally found a way that seemed to be less complicated than I thought. I would like to share it with you, but please be advised, however, that it’s experimental only. There are few browser-related situations where it malfunctions.

Create or Find any Image

CSS teddy bear

The first step is to draw everything like usual or grab an existing image. For this demo, we’ll use the teddy bear I drew above. Here’s its HTML:

<div class="teddy">
  <div class="right-ear"></div>
  <div class="left-ear"></div>
  <div class="head">
    <div class="right-eye"></div>
    <div class="left-eye"></div>
    <div class="mouth-area">
      <div class="right-cheek"></div>
      <div class="left-cheek"></div>
      <div class="nose">
        <div class="nose-inner"></div>
      </div>
    </div>
  </div>
  <div class="body"></div>
  <div class="right-arm">
    <div class="arm-inner"></div>
    <div class="paw"></div>
  </div>
  <div class="left-arm">
    <div class="arm-inner"></div>
    <div class="paw"></div>
  </div>
  <div class="right-leg">
    <div class="foot"></div>
  </div>
  <div class="left-leg">
    <div class="foot"></div>
  </div>
</div>

Then, we’ll use the following CSS:

.teddy {
	position: relative;
	margin: auto;
	width: 260px;
	height: 330px;
}
.teddy div {
	position: absolute;
}
.right-ear, .left-ear, .head, .right-eye, .left-eye, .mouth-area, .nose, .nose-inner, .right-cheek, .left-cheek, .body, .paw, .foot {
	border-radius: 50%;
}
.right-ear, .left-ear {
	background: rgb(95,60,30);
	box-shadow: 0 0 0 20px rgb(190,120,60) inset;
	width: 80px;
	height: 80px;
}
.right-ear {
	left: 30px;
}
.left-ear {
	right: 30px;
}
.head {
	background: rgb(190,120,60);
	width: 160px;
	height: 160px;
	right: 0;
	left: 0;
	margin: auto;
	z-index: 1;
}
.right-eye, .left-eye, .nose-inner {
	background: rgb(255, 255, 255);
}
.right-eye, .left-eye {
	box-shadow: 2px -2px 0 8px inset;
	top: 65px;
	width: 20px;
	height: 20px;
}
.right-eye {
	left: 45px;
}
.left-eye {
	right: 45px;
}
.mouth-area {
	background: rgb(230,160,90);
	right: 0;
	bottom: 5px;
	left: 0;
	margin: auto;
	width: 100px;
	height: 70px;
}
.nose {
	background: rgb(0,0,0);
	top: 15px;
	right: 0;
	left: 0;
	margin: auto;
	width: 40px;
	height: 30px;
}
.nose-inner {
	top: 3px;
	right: 8px;
	width: 10px;
	height: 8px;
}
.right-cheek, .left-cheek {
	bottom: 10px;
	width: 30px;
	height: 30px;
}
.right-cheek {
	box-shadow: -2px -2px 0 0 rgb(115,80,45) inset;
	left: 20px;
}
.left-cheek {
	box-shadow: 2px -2px 0 0 rgb(115,80,45) inset;
	right: 20px;
}
.body {
	background: rgb(230,160,90);
	box-shadow: 0 0 0 24px rgb(190,120,60) inset;
	margin: auto;
	right: 0;
	bottom: 22px;
	left: 0;
	width: 170px;
	height: 190px;
}
.right-arm, .left-arm {
	background: rgb(170,100,50);
	top: 130px;
	width: 70px;
	height: 60px;
}
.right-arm {
	border-radius: 70px 0 0 0 / 60px 0 0 0;
	left: 0;
}
.right-arm .arm-inner {
	border-radius: 50px 0 0 0 / 80px 0 0 0;
	box-shadow: 22px 22px 0 0 rgb(170,100,50) inset;
	left: 55px;
}
.left-arm, .left-arm .paw {
	right: 0;
}
.left-arm {
	border-radius: 0 70px 0 0 / 0 60px 0 0;
}
.left-arm .arm-inner {
	border-radius: 0 50px 0 0 / 0 80px 0 0;
	box-shadow: -22px 22px 0 0 rgb(170,100,50) inset;
	right: 55px;
}
.arm-inner {
	width: 50px;
	height: 80px;
	bottom: 0;
}
.paw {
	background: rgb(190,120,60);
	width: 80px;
	height: 60px;
	top: 30px;
}
.right-leg, .left-leg {
	background: rgb(170,100,50);
	border-radius: 45px 45px 0 0 / 70px 70px 0 0;
	bottom: 40px;
	width: 90px;
	height: 70px;
}
.right-leg  {
	left: 10px;
}
.left-leg  {
	right: 10px;
}
.foot  {
	background: rgb(230,160,90);
	box-shadow: 0 5px 0 10px rgb(190,120,60) inset;
	width: 100%;
	height: 80px;
	top: 30px;
}

Before we start pulling off the trick, verify that the parent container has a defined width and height. They must use an absolute unit (px, pt, pc, in, cm, or mm) in order to maintain a constant side-to-side ratio. Any selectors of child elements using absolute units must use the same type as that of the parent. In this case, .teddy is our parent container at 260px × 330px, and we have left the %s under .right-ear,..., .foot and .foot alone since they weren’t absolute.

Applying the Trick

Once we’re satisfied with the code, let’s add a font size to the container using a calc() expression. It shall divide a new width or height of the image by the original one. Just don’t add the unit to the original one because the divisor in calc() division must be a number. For our example we’ll choose a new width of 50vmin (for responsiveness) and divide it by the original width 260 (px dropped).

.teddy {
	font-size: calc(50vmin / 260);
	. . .
}

We could also use an expression that adds or subtracts different units as well like (300px + 5vw) as long as we keep everything before the slash and 260 in parentheses. %s used here though applies to font size only and yields different results in every browser.

After setting the font size, we replace every absolute unit out side of the calc() with em (you can speed up this process using your text editor’s Find and Replace tool). In this example we used px and replaced each px with em.

.teddy {
	font-size: calc(50vmin / 260);
	position: relative;
	margin: auto;
	width: 260em;
	height: 330em;
}
.teddy div {
	position: absolute;
}
.right-ear, .left-ear, .head, .right-eye, .left-eye, .mouth-area, .nose, .nose-inner, .right-cheek, .left-cheek, .body, .paw, .foot {
	border-radius: 50%;
}
.right-ear, .left-ear {
	background: rgb(95,60,30);
	box-shadow: 0 0 0 20em rgb(190,120,60) inset;
	width: 80em;
	height: 80em;
}
.right-ear {
	left: 30em;
}
.left-ear {
	right: 30em;
}
.head {
	background: rgb(190,120,60);
	width: 160em;
	height: 160em;
	right: 0;
	left: 0;
	margin: auto;
	z-index: 1;
}
.right-eye, .left-eye, .nose-inner {
	background: rgb(255, 255, 255);
}
.right-eye, .left-eye {
	box-shadow: 2em -2em 0 8em inset;
	top: 65em;
	width: 20em;
	height: 20em;
}
.right-eye {
	left: 45em;
}
.left-eye {
	right: 45em;
}
.mouth-area {
	background: rgb(230,160,90);
	right: 0;
	bottom: 5em;
	left: 0;
	margin: auto;
	width: 100em;
	height: 70em;
}
.nose {
	background: rgb(0,0,0);
	top: 15em;
	right: 0;
	left: 0;
	margin: auto;
	width: 40em;
	height: 30em;
}
.nose-inner {
	top: 3em;
	right: 8em;
	width: 10em;
	height: 8em;
}
.right-cheek, .left-cheek {
	bottom: 10em;
	width: 30em;
	height: 30em;
}
.right-cheek {
	box-shadow: -2em -2em 0 0 rgb(115,80,45) inset;
	left: 20em;
}
.left-cheek {
	box-shadow: 2em -2em 0 0 rgb(115,80,45) inset;
	right: 20em;
}
.body {
	background: rgb(230,160,90);
	box-shadow: 0 0 0 24em rgb(190,120,60) inset;
	margin: auto;
	right: 0;
	bottom: 22em;
	left: 0;
	width: 170em;
	height: 190em;
}
.right-arm, .left-arm {
	background: rgb(170,100,50);
	top: 130em;
	width: 70em;
	height: 60em;
}
.right-arm {
	border-radius: 70em 0 0 0 / 60em 0 0 0;
	left: 0;
}
.right-arm .arm-inner {
	border-radius: 50em 0 0 0 / 80em 0 0 0;
	box-shadow: 22em 22em 0 0 rgb(170,100,50) inset;
	left: 55em;
}
.left-arm, .left-arm .paw {
	right: 0;
}
.left-arm {
	border-radius: 0 70em 0 0 / 0 60em 0 0;
}
.left-arm .arm-inner {
	border-radius: 0 50em 0 0 / 0 80em 0 0;
	box-shadow: -22em 22em 0 0 rgb(170,100,50) inset;
	right: 55em;
}
.arm-inner {
	width: 50em;
	height: 80em;
	bottom: 0;
}
.paw {
	background: rgb(190,120,60);
	width: 80em;
	height: 60em;
	top: 30em;
}
.right-leg, .left-leg {
	background: rgb(170,100,50);
	border-radius: 45em 45em 0 0 / 70em 70em 0 0;
	bottom: 40em;
	width: 90em;
	height: 70em;
}
.right-leg  {
	left: 10em;
}
.left-leg  {
	right: 10em;
}
.foot  {
	background: rgb(230,160,90);
	box-shadow: 0 5em 0 10em rgb(190,120,60) inset;
	width: 100%;
	height: 80em;
	top: 30em;
}

Finally, resize the window to see how the image increases or decreases in size proportionally!

CSS teddy bear made responsive
(Demo)

If you got something awfully strange, refactor your code so that every element is inside one main container element with an absolute width and height. Ensure that you used a single type of absolute unit, too. If you feel you did everything correctly and the image still appears larger than it should, I’ll discuss why that happens in the next section.

Caveats

Whether this successfully worked or not, there are still two caveats to be aware of:

The Minimum Font Size Setting

If your image ended up being unusually large, here’s why: the browser has a setting for minimum font size, and it will force the result of the calc() we used to that font size if under. For instance, if the resulting font size is 2px and the minimum is 6px, then it remains at 6px. This will make every em 6px each thus causing the image to be ridiculously large. I discovered this by accident when for no reason I played with Chrome’s font settings. Since the default minimum font size is 1px and we did not touch that setting for this demo, we could use super small font size for resizing a CSS image in the way desired.

Additionally, according to some post about minimum font size in Chrome by Ian Lai, we would have been able to fix the minimum font size issue in every browser using the text-size-adjust property. Unfortunately, desktop browser support has been dropped a few years a ago (possibly due to accessibility issues).

calc() with Viewport Units in Safari

The other caveat is that if you provide a new length using vw, vh, vmin, or vmax, Safari will only calculate it on load and not on window resize. This happens to be a bug with using viewport units in calc() for font size, and there currently aren’t any workarounds I know of.

How It Works

The magic behind this is all in the font size and em. Since 1em is equal to the font size of the current element, font size will redefine what one unit of the image would be if we switched the units for everything else from that unit to em. If the font size were 10px for instance, every pixel of the image will be 10 times larger. Then, an image originally 50px wide would become 500px.

To show how we can choose a new width or height and get what we want though, we use calc(new_length / original_length) (remember, drop the unit for the original length!). Plus, it really doesn’t matter which absolute unit we choose to work with as long as we’re being consistent.

Conclusion

In a nutshell, we can enable CSS images to be proportionally resizable and responsive by:

  1. Using calc(new_length / original_length) for the parent container’s font size
  2. Swapping every absolute unit outside the calc() with em

Although there’s still some experimenting to do for improving the safety of use in production, this has been a huge step towards making pure CSS images as flexible as <img> elements and SVGs. This approach for responsive CSS images is the best I’ve found so far, and I hope you’ll enjoy trying it out!


Posted in: CSS