Skip to content
DevDepth

In-depth React internals and CSS layout guides for frontend engineers.

← Back to all articles

In-Depth Article

How to Make Text Over Images Readable in CSS

Learn how to make text over images easier to read with scrims, overlays, blur, text highlights, and accessible contrast checks, plus a clean CSS overlay layout pattern.

Published: Updated: 8 min readlayout-strategy
image-overlaycss-gradientstext-shadowaccessibilitylayout-strategy

Text over images is one of those UI patterns that looks simple in a mockup and fragile in production.

The design looks great with the exact photo from the presentation. Then the real image arrives with brighter highlights, more visual noise, or a failed load state, and suddenly the headline is hard to read or nearly invisible.

That is why "put white text on top of a photo" is not really a styling trick. It is a readability problem.

This guide focuses on the practical ways to solve that problem in modern CSS:

  • how to build the overlay layout cleanly
  • when to use a scrim, a full overlay, blur, grayscale, or a text highlight
  • why text shadows help but should not do all the work
  • how to think about contrast before shipping
  • how to plan for broken or missing images instead of treating them as impossible

If you want the broader responsive image baseline first, start with Responsive Images in CSS: aspect-ratio, object-fit, and image-rendering. If the overlay also depends on shadow treatment, How CSS Shadows Really Work: text-shadow, box-shadow, and drop-shadow() is the best companion page.

1. Readable text over images always has three layers

Most workable versions of this pattern use the same stack:

  1. the image
  2. a control layer between the image and the text
  3. the text itself

That middle layer is what makes the component reliable.

Sometimes it is:

  • a dark gradient
  • a translucent solid color
  • a blur treatment
  • a mixed overlay
  • a localized text highlight behind only the caption

Without that layer, the text is forced to compete directly with whatever random contrast the image happens to provide.

2. Build the overlay layout first, then decide the readability treatment

One clean way to layer image and text is CSS Grid:

<figure class="card">
  <img src="./elephant.jpg" alt="Elephant walking at sunset" />
  <figcaption>An elephant at sunset</figcaption>
</figure>
.card {
  display: grid;
}

.card > * {
  grid-area: 1 / 1;
}

.card img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.card figcaption {
  align-self: end;
  z-index: 2;
  padding: 1rem;
  color: white;
}

This gives you the basic overlay structure without extra presentational wrappers.

Then, if you need a readability layer, add it with a pseudo-element:

.card::before {
  content: "";
  grid-area: 1 / 1;
  z-index: 1;
}

That keeps the markup honest:

  • image content stays in the image
  • caption content stays in the caption
  • readability treatment stays in CSS

If overlay positioning starts behaving strangely, the bug is often stacking-related rather than image-related. In that case, Why z-index Is Not Working and How to Fix It is the right follow-up.

3. A dark gradient scrim is usually the best default

If you only want one technique that works well most of the time, start here.

A scrim is a translucent gradient behind the text, usually darker near the caption and transparent farther away. It protects readability without flattening the entire image.

.card::before {
  content: "";
  grid-area: 1 / 1;
  background-image: linear-gradient(
    to top,
    rgb(0 0 0 / 0.65),
    rgb(0 0 0 / 0) 45%
  );
  z-index: 1;
}

Why it works well:

  • it darkens the part of the image that matters most for the text
  • it preserves more of the photo than a full overlay
  • it usually feels more intentional than a heavy text shadow alone

This is especially strong for:

  • hero cards
  • editorial thumbnails
  • image captions placed near the bottom edge

4. Full-image overlays are stronger, but they flatten the image more

Sometimes the image is too busy, or the text is simply more important than the photography.

In those cases, a translucent color over the whole image can be the right move:

.card::before {
  content: "";
  grid-area: 1 / 1;
  background-color: rgb(0 0 0 / 0.5);
  z-index: 1;
}

This is the blunt instrument version of the pattern.

Use it when:

  • readability is the top priority
  • the image is mostly decorative
  • the text block is large or dense
  • the visual system wants a more controlled, branded look

The tradeoff is obvious: the image loses more of its original depth and color.

That is not always a problem. It just means the image is no longer the main actor.

5. Mixed overlays often give you the most forgiving result

In production UI, one overlay layer is often not enough. A small solid tint plus a directional gradient can be more forgiving across inconsistent images.

.card::before {
  content: "";
  grid-area: 1 / 1;
  background-color: rgb(0 0 0 / 0.2);
  background-image: linear-gradient(
    to top,
    rgb(0 0 0 / 0.55),
    rgb(0 0 0 / 0) max(25%, 40%)
  );
  z-index: 1;
}

This approach helps because:

  • the base tint quiets the whole image slightly
  • the gradient adds extra protection where the text sits

It is a good choice when the image library is inconsistent and you want fewer edge-case failures.

6. Adjust the protected area, not just the opacity

One common mistake is to keep increasing overlay opacity until the text becomes readable.

That works, but it often makes the card feel muddy.

A better first move is often to increase the coverage of the readable zone instead of only darkening it more.

For example, if the caption block is taller on larger cards, let the gradient extend farther upward:

.card::before {
  content: "";
  grid-area: 1 / 1;
  background-image: linear-gradient(
    to top,
    rgb(0 0 0 / 0.6),
    rgb(0 0 0 / 0) 55%
  );
  z-index: 1;
}

This is often better than turning a 40% overlay into a 75% overlay everywhere.

The design lesson is small but important:

readable text over images is not only about darkness. It is also about how much of the image is being controlled.

7. Text highlights are excellent for smaller labels or short headlines

Sometimes the cleanest solution is not to treat the whole image. Treat the text itself.

.card figcaption {
  align-self: end;
  z-index: 2;
  color: white;
  background-color: rgb(0 0 0 / 0.7);
  padding: 0.375rem 0.625rem;
  width: fit-content;
}

This is especially effective when:

  • the text is short
  • the caption sits in a loose image composition
  • you want the photo to remain more visible overall

It often feels more deliberate than a giant full-card overlay when the text block is small.

You can also tint the highlight with a brand color, as long as the final text contrast still holds up.

8. Blur and grayscale can help, but they are supporting tools

Sometimes the image itself needs to become quieter.

Blur

Blur reduces visual detail and can make text easier to read:

.card::before {
  content: "";
  grid-area: 1 / 1;
  backdrop-filter: blur(8px);
  background-color: rgb(255 255 255 / 0.08);
  z-index: 1;
}

This can work well for:

  • glassmorphism-like overlays
  • larger promotional banners
  • UI that already embraces a softened visual treatment

But blur is not free. It can cost more to render and should not be your first fix for every caption problem.

Grayscale or muted image treatment

Quieting the image can also help:

.card img {
  filter: grayscale(1);
}

This can reduce color competition, but it does not guarantee readable contrast on its own. A grayscale image can still be too light or too dark behind the text.

So use blur or grayscale as image treatments, not as a substitute for a real readability layer.

9. Text shadow helps, but it should rarely be the only fix

Text shadow can increase local edge contrast:

.card figcaption {
  color: white;
  text-shadow: 0 2px 4px rgb(0 0 0 / 0.35);
}

That is often useful as a finishing layer.

What it should not do is carry the whole readability job by itself on a noisy image.

If the underlying photo is highly variable, text shadow alone usually feels like a patch rather than a system. Pair it with:

  • a scrim
  • a solid overlay
  • a text highlight

That combination ages much better.

10. Contrast is the rule, not the mood

A beautiful overlay is still a weak solution if the text contrast remains unreliable.

In practice, treat image overlays as an accessibility problem as much as a visual one. For normal text, you usually want the text and its effective background treatment to meet WCAG contrast expectations. That typically means stronger contrast than many first-draft mockups provide.

The safest workflow is:

  1. choose the text color
  2. add the overlay treatment
  3. test the real result on several real images
  4. increase coverage or opacity only as much as needed

Design tools and browser DevTools can help here, but the important habit is simpler: do not test on only one perfect demo image.

If the component will receive unpredictable photography, your overlay needs to survive unpredictability too.

11. Broken or missing images need a plan too

An image overlay component can fail even when the CSS is correct, simply because the image does not load.

That means you should design the empty or broken state as part of the component.

A reliable baseline is:

  • give the media area a fallback background color
  • keep the text readable without depending entirely on the image
  • let the overlay treatment still make sense when only the fallback color shows

For example:

.card {
  display: grid;
  background: #2b2f3a;
}

.card > * {
  grid-area: 1 / 1;
}

.card img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

If the image fails, the component is not pretty photography anymore, but it is still a usable block with readable text.

You can go further with explicit placeholders, fallback icons, or server-generated fallbacks. I would treat those as component decisions rather than relying on fragile browser-specific hacks around broken images.

12. Choose the technique by the job

Here is the practical selection guide:

Use a scrim when:

  • the text sits near one edge
  • you want to preserve most of the image
  • the component is editorial or card-like

Use a full overlay when:

  • text is more important than the image
  • the image is very noisy
  • you need more predictable contrast

Use a mixed overlay when:

  • image quality varies a lot
  • one technique alone feels too weak
  • you need more resilience across a whole content library

Use a text highlight when:

  • the text block is short
  • you want the image to stay more visible
  • the caption should feel like a label

Use blur or grayscale when:

  • quieting the image supports the design language
  • performance cost is acceptable
  • you still pair it with a real readability treatment

13. A compact checklist before you ship

Before publishing a text-over-image component, check:

  1. Is the image content meaningful, or should it really be decorative CSS?
  2. Does the layout reserve stable image space with aspect-ratio or an equivalent pattern?
  3. Is the text readable across several very different real images?
  4. If the text is white, is the darkening treatment actually strong enough where the text sits?
  5. If the image fails to load, does the component still look intentional and readable?
  6. Is text shadow only a helper, rather than the entire solution?

That checklist catches most of the failure modes that mockups hide.

14. The takeaway

Making text readable over images is not about one magical CSS property. It is about controlling the relationship between the photo, the readability layer, and the caption.

If you keep one rule from this page, make it this one:

never ask text to compete directly with a busy image if readability matters.

Use Grid or another clean overlay layout to stack the layers.

Use a scrim first when you want the best balance between readability and image preservation.

Escalate to a full overlay, mixed treatment, or text highlight when the component needs more reliability.

And always test the real result on messy, imperfect images instead of the one photo that made the design look easy.

Publisher and editor

DevDepth Publisher

Independent publisher and frontend engineering writer

DevDepth is maintained as an independent frontend engineering publication focused on React internals, CSS layout systems, and production debugging guides.

React internalsCSS layout systemsfrontend debuggingrendering and performance reasoning

Each article is reviewed before publication for technical accuracy, explanation quality, metadata clarity, and internal-link fit within the current archive.

Last editorial review: Mar 20, 2026