Treat images deliberately

Overlay under text, crop don't stretch, and give avatars a fallback.

Steve Schoger@steveschoger
🔥 Adding a subtle shadow to white text when on a bright background not only makes it more legible but helps it 'pop' more.

Weekend in Lisbon

Measure it in demo 1
Jun 29, 2017View on X ↗
Steve Schoger@steveschoger
🔥 If you're working with images that sometimes bleed into the background, try using a subtle inner shadow to create some distinction instead of a border. Borders will often clash with the image, while most people will barely realize the shadow is even there.
See it in demo 2
Nov 19, 2018View on X ↗

From refactoring-ui, Images & Icons. Contrast is measured from real pixels.

Text over a photo

Switch photos, then pick a treatment in the table.

Weekend in Lisbon Three nights by the river, from $480
Lowest contrast under the text, per treatment
TreatmentHeadline 3:1Subline 4.5:1
.hero { position: relative; aspect-ratio: 16 / 9; overflow: hidden; }
.hero img { width: 100%; height: 100%; object-fit: cover; }
.hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgb(0 0 0 / .6), transparent);
}
.hero-copy { position: absolute; z-index: 1; left: 20px; bottom: 18px; color: #fff; }
@media (prefers-color-scheme: dark) {
  .hero img { filter: brightness(0.9) contrast(1.1); }
}

Mixed-size uploads in one grid

Drag the card width, then change the ratio.

Stretched to fitBad
img { width: 100%; height: 120px; }
aspect-ratio + coverGood
aspect-ratio: 16 / 9; object-fit: cover
.card-media { position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 8px; }
.card-media img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
.card-media::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .1);
  pointer-events: none;
}

Avatars that always render

Break the photo URLs, or add a name.

Raw <img src>Bad
    Assigned
    <img src={user.avatarUrl}>
    Cover + initialsGood
      Assigned
      onerror → initials · ring-2 ring-card
      .avatar { width: 40px; height: 40px; flex: none; overflow: hidden;
        display: grid; place-items: center; border-radius: 50%;
        background: var(--muted); font-weight: 600; font-size: 16px; }
      .avatar[data-kind="company"] { border-radius: 25%; }
      .avatar img { width: 100%; height: 100%; object-fit: cover; }
      .stack .avatar { box-shadow: 0 0 0 2px var(--card); }
      
      img.addEventListener("error", () => img.replaceWith(initials(name)), { once: true });

      Why it matters, pros and cons

      Why it matters

      You don't control photos, ratios or avatar URLs. A frame, a crop and a fallback make the result the same every time.

      Pros

      • Text stays readable on any photo.
      • Grid rows keep one height.
      • No broken icons when a URL fails.

      Cons

      • An overlay darkens a photo that is the product.
      • cover can crop a face without a focal point.
      • Initials collide, so keep the name nearby.