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
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.
From refactoring-ui, Images & Icons. Contrast is measured from real pixels.
Text over a photo
Switch photos, then pick a treatment in the table.
| Treatment | Headline 3:1 | Subline 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.
covercan crop a face without a focal point.- Initials collide, so keep the name nearby.