Speed Patterns octocat github link burger menu

Use Styles Instead Of Images

By Sergey Chernyshev · · Assisted by AI

Visual effects that originate in design tools — drop shadows, gradients, rounded corners, decorative borders, even small icons — are often shipped to the browser as raster images. They didn't have to be. CSS can express most of those effects directly in markup, at a tiny fraction of the byte cost and with no loss of fidelity.

The Problem

A typical handoff path looks like this: the designer builds a mockup in Figma or Photoshop, exports the visual elements as PNG or JPEG, and the developer drops the exports into the page. The result looks right, but it's expensive:

Every image is a network round-trip, a decode step on the user's CPU, and a layout consideration. Avoiding the round-trip entirely is almost always cheaper.

The same visual element shipped two ways: as a 142KB raster image, or as ~120 bytes of CSS. The CSS version is also crisp at any DPI and animatable.

Side-by-side comparison: a pixelated raster image labeled '142 KB, blurry on retina, slow to render' next to a smooth rounded rectangle labeled 'CSS: ~120 B, crisp at any DPI, animatable'
Pixels vs. CSS

Solution

Make CSS the default for any visual element that isn't fundamentally photographic. Reach for an image only when the content is a real photo or a complex illustration that CSS truly can't express.

What CSS handles well

If your designer says "I want this corner rounded with a soft shadow and a thin border" — that's three lines of CSS and zero images.

Anti-pattern: the mixed hero

A common offender is a hero banner that combines a photographic image with overlaid text. Designers often deliver this as a single rendered image — and the result is bad on both axes:

The right answer is to split the layers: keep the photo as a compressed image and render the text as real HTML positioned over it. The photo can be aggressively compressed (it's only being judged as a photo). The text stays crisp, accessible, responsive, and editable without shipping a new image.

Why This Works

CSS rules are part of the stylesheet — already cached, already parsed, already applied to many elements. Replacing an image with CSS:

Guidelines

Related Patterns

Technical Implementation

Splitting the mixed hero

The mixed-hero anti-pattern is fixed by separating the photographic layer from the typographic one:

<section class="hero">
  <img src="/assets/hero-photo.jpg" alt="" class="hero__photo" />
  <div class="hero__copy">
    <h1>Crisp, real text</h1>
    <p>Selectable, translatable, accessible.</p>
  </div>
</section>
.hero {
  position: relative;
}
.hero__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero__copy {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  color: white;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}

Each layer is now optimized for its own job: the JPEG can compress freely, and the text remains crisp, indexable and translatable.

CSS instead of decorative images

Most decorative effects that once required images are direct CSS now: box-shadow, border-radius, linear-gradient(), radial-gradient(), conic-gradient(), backdrop-filter, mask-image and the filter family cover almost all of what design tools export. When in doubt, prototype the effect in CSS first; only fall back to a raster export if the browser truly can't reproduce it.