The Zen of Borders and Box Shadows

No one likes a shifting layout. Designers hate that it decimates the first impression they worked hard to craft. Frequent users hate it because it stalls their standard interactions. And I hate it because the Lighthouse score knocks my delicate ego.

Over the years, one CSS element caused 75 percent of my layout-shifting woes. That element is border. I use them frequently, but they're delicate, particularly on hover.

Then I found box-shadow, and it took me in like a siren. Here's the fun bit. Box-shadow causes zero layout shift. None at all. Let me demonstrate.

import "./styles.css";
import React from "react";

export default function App({ border = "1", boxShadow = "1" }) {
  return (
    <div className="App">
      <h1 style={{ padding: "5px" }}>My Neat Blog</h1>
      <div className="card-container">
        <div className="card">
          <span>Border</span>
        </div>
        <div className="card2">
          <span>Box-Shadow</span>
        </div>
      </div>
      <div className="content">
        <h2> My neat blog content </h2>
      </div>
    </div>
  );
}

Now, I'll be entirely honest here. When I discovered this, I had no earthly idea why the two behaved differently. It turned out quite a bit was at play.

Borders aren't simply broken. We'll cover how to fix that ugly shift, but we'll also explore box-shadow and the pros and cons of the two solutions.

Let's start by breaking down the box-shadow CSS element. CSS offers several short-hand properties that apply myriad styles within a single variable. Variables like border, animation, margin, and box-shadow.

The Nitty Gritty of Box Shadow

Box-shadow aggregates five properties:

    • offset-x moves the shadow along the x-axis.
    • offset-y advances the shadow along the y-axis.
    • blur-radius defines how much the shadow blurs around its edges.
    • spread-radius increases the size of the blur, making the shadow larger and brighter.
    • color is (surprisingly) our shadow's color.

Like border, any of these properties can be excluded. For our purposes, CSS interprets box-shadow as follows:

    • If two values are provided, CSS interprets them as offset-x and offset-y.
    • If a third value is present, it represents the blur-radius.
    • And if a fourth is included, it defines our spread-radius.

Say we had box-shadow: 0 0 2px 2px red;. Without x or y-axis offsets (set as 0 0), our shadow sits directly behind our element. It's slightly blurred and extends 2px beyond the edges of its target.

This syntax enables a nifty little maneuver. What if we only set spread-radius and color? The result is, more or less, the same as a border, a flat line that encompasses our element.

There is a slight visual difference. See if you can spot it.

Border card.
Box Shadow Card
Adjust border:
Adjust shadow:

A CSS border "grows" inwards, while a box-shadow expands outwards. That makes sense, but it's not something I'd considered. However, that alone doesn't explain why they behave differently.

The next section covers the differences in full, but before we move on, let's cover some of box-shadow's advantages and disadvantages. The advantages mostly funnel into choice of design, but there is one major performance uptick.

A `box-shadow` creates the depth and deminsionality that a `border` cannot.
They add contrast to visually separate two similar elements.
Or style them to mimic the appearance of a border, without altering the element's size.

Most importantly, box-shadows don't trigger geometry or layout changes. This includes neighboring components, as well as the inner contents of the shadowed element. If you're a blogger, that means no moving pictures or squashed text on hover!

Most of the "advantages" are cosmetic (not all, just most!). Unfortunately, its disadvantages can negatively affect performance. Like all CSS paints and animations, rendering box-shadows can be costly. Even more so for large elements, as the browser may force re-paints while scrolling.

At least, that's what articles from 2018 kept telling me. But nowadays, most of that is offloaded to the GPU, so your animations aren't as nearly as power-hungry as they used to be. There's such a thing as too much, but your inner designer, or real designer, will stop you before it's noticeable.

That said, the same goes for any CSS animation. If you're using changing box-shadow or border on hover, similar to the above, then the differences in performance are negligible.

The Anatomy of Border and Border-box

As HTML tends to go, it starts with a div. A div doesn't begin with any inherent border, and if one is applied, it lands on the div's outer edge by default. It might be a "me" thing, but this feels wrong. This quote by Peter Paul Koch says it better than I could.

"Logically, a box is measured from border to border. Take a physical box, any box. Put something in it that is distinctly smaller than the box. Ask anyone to measure the width of the box. He'll measure the distance between the sides of the box (the 'borders'). No one will think of measuring the content of the box."

Web development is mostly just a bunch of boxes. We size boxes, shape boxes, and then piece them together to craft a functioning interface. We aren't always concerned with measurements of the contents of these boxes, but we always consider the actual dimensions of said box.

To remedy this, we'll utilize one of CSS's "newer" features: box-sizing: border-box;. Remember, divs have no border by default.Border-box creates this inner edge on which to place one.

Back up to our earlier example, and uncomment the box-sizing line to see its effect live. That said, this alleviates one issue but not both. Border-box maintains the layout of its surrounding neighbors, but as borders grow inward, it still affects the sizing of its inner contents.

So, we're still left with the component-level shift. It's lightyears ahead of the horrors of shifting neighbors, but it still feels lacking.

Note that we can correct this as well, but that's beyond the scope of this article. We'll cover these pseudo classes in a follow-up tutorial.

Should I Use Box-shadow or Border?

The short answer is to use whichever you prefer!

After a good deal of testing, neither is "better." I use both, as both have their uses. For instance, my article cards have an overlay showing the post category. I feel box-shadow works best here, as the card itself is small, and I'd rather not risk the text getting squashed or moved if some condition occurs that I didn't consider.

The same goes for the article cards themselves. I prefer the aesthetic and didn't notice a performance difference when using box-sizing. But at the end of the day, development is about creation. What I think looks fantastic could be the sloppiest design you've seen in your entire life.

So, try out both approaches and find which one suits you. Worse comes to worst, at least you added a new tool to the 'ol box.

But if you ever find yourself in need of some layering, box-shadow has a couple tricks to spare.

Box-shadow's Roads Less Traveled

Border only let's us define a singular border. Box-shadow lets you run wild.

css
1 box-shadow: 2px 4px 0 4px hsl(230deg, 100%, 69%),
2 4px 8px 0 8px hsl(310, 68%, 70%),
3 8px 10px 0 12px hsl(170, 38%, 48%);
4
"You miss 100% of the shots you don't take. — Wanye Gretzsky — Michael Scott"

You might be wondering what exactly that's useful for. Honestly, I have no idea. You could play with the opacity to add a bit more depth, and it's certainly worth a look if you're ever tasked with building drag-and-drop-styled card components.

Either way, hopefully, someone out there sees potential in this strange multi-shadow card jutsu (I cut this and put it back around ten times. I'm weak).

We typically use box-shadows to add depth behind an element. These drop shadows add an illusion of dimensionality to an otherwise static webpage, making it appear as if our element casts a shadow.

These layered-shadows offer maximum lift, but what if need depth? While offset moves our shadow along the x or y-axis, inset sets the shadow inside the frame. Like all box-shadows these can be chained.

import "./styles.css";
import MultiShadowCard from "./MultiShadowCard";
import InsetShadowCard from "./InsetShadowCard";

export default function App() {
  return (
    <div className="App">
      <div className="card-container">
        <MultiShadowCard text={"Multiple shadows for maximum lift."} />
        <InsetShadowCard text={"Inset for maximum depth."} />
      </div>
    </div>
  );
}

Our element sinks into the frame. To be fair, these are no secret. I see them in the wild regularly, but they're more useful than you might think. An element could "cast" a shadow on its neighbor, emphasize a blockquote, or give an image a vignette effect via a div wrapper.

It's also worth noting that I did run into a small 'gotcha' while making the above components. As box-shadow doesn't add to the size of an element, I had to double the bottom margin to keep it from bleeding onto the text. Something to keep in mind, particularly in a mobile environment.

To Wrap

Box-shadows do not affect the size of an element, but like all CSS animations, they come with some rendering overhead. Borders will alter an element's dimensions, but this can be partially remedied via box-sizing.

Neither property trumps the other, but if I had to choose, I'd roll with box-shadow. That may change as we dive further into pseudo-classes, but as it stands, box-shadow's versatility is tough to beat.

Like always, thanks for reading and perhaps even enjoying it! Feel free to reach out with any feedback you may have! I'm working hard to craft as interactive an experience as I can. I'd love to hear what works and what doesn't!