# Overlay text on an image with CSS: three layouts and how to keep it readable

> Put text over an image with CSS — absolute positioning, grid stacking or a background image — and the overlay white text needs to pass WCAG contrast.

Published 2026-11-07 · https://shareglint.com/en/guides/overlay-text-on-image-css

Text over a photo is the most common pattern in web design after the paragraph: hero banners, article cards, product tiles, social images. CSS has three clean ways to lay it out, and one rule that decides whether anyone can read the result: **contrast**. This page shows the three layouts, then measures how strong an overlay has to be for white text to pass WCAG contrast. The contrast numbers were measured in Chrome (Puppeteer 25.7.0, Chrome 152) on 2026-11-07.

## 1. Absolute positioning

The classic layout: a container that positions its children, the image filling it, the text placed on top.

```html
<figure class="card">
  <img src="forest.jpg" alt="Fog over a beech forest at sunrise">
  <figcaption class="card-text">Morning light in the old beech forest</figcaption>
</figure>
```

```css
.card { position: relative; aspect-ratio: 16 / 9; margin: 0; overflow: hidden; }
.card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-text {
  position: absolute; inset: auto 0 0 0;       /* stick to the bottom edge */
  padding: 1.5rem; color: #fff; font-size: 1.75rem;
}
```

`object-fit: cover` crops the photo to fill the box without stretching it ([MDN: object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)); `object-position` chooses which part survives the crop — `object-position: 50% 20%` keeps faces near the top. `display: block` removes the small gap an inline image leaves under itself.

The catch: absolutely positioned text takes no space. If the text is longer than the box, the card does not grow: in our test a 640×360 card held 592 px of text, and `overflow: hidden` cut off everything above the card's top edge. Fine for a fixed-size banner with short text, wrong for a card whose text length varies.

## 2. Grid stacking

Put both children into the same grid cell. Nothing is taken out of the flow, so the card grows with its text:

```css
.card { display: grid; margin: 0; }
.card > * { grid-area: 1 / 1; }             /* image and text share one cell */
.card img { width: 100%; height: 100%; min-height: 16rem; object-fit: cover; }
.card-text { align-self: end; padding: 1.5rem; color: #fff; z-index: 1; }
```

`align-self: end` puts the text at the bottom, `center` in the middle. The cell is as tall as the taller child, so long text makes the card taller instead of overflowing — with the same 592 px of text, the grid card grew from 360 to 592 px. This is the layout to use for cards in a list.

## 3. Background image

When the photo is decoration, not content, it can be the container's background:

```css
.hero {
  min-height: 24rem; display: grid; place-items: center; padding: 2rem;
  background: url("forest.jpg") center / cover no-repeat;
  color: #fff; text-align: center;
}
```

`background-size: cover` (the `/ cover` in the shorthand) crops like `object-fit: cover`. A background image has no alt text and is not announced by screen readers, so use it only when the picture adds nothing a reader would miss.

## Keeping the text readable

White text on a photo fails exactly where the photo is light: a sky, a white wall, snow. WCAG 2.2 asks for a contrast ratio of at least **4.5:1** for normal text and **3:1** for large text (24 px, or 18.66 px bold) ([WCAG 2.2, Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)). Where the background varies, as a photo does, WCAG's technique G18 says the background around the letters can be "chosen or shaded" so that the letters keep that ratio against what is behind them ([G18](https://www.w3.org/WAI/WCAG22/Techniques/general/G18)).

The reliable fix is a dark layer between the photo and the text. We rendered a black overlay at different opacities over three backgrounds and measured white text's contrast against the resulting pixel:

| Black overlay | over white #ffffff | over yellow #ffd54f | over sky blue #87ceeb |
|---|---|---|---|
| none | 1.00:1 | 1.41:1 | 1.74:1 |
| `rgba(0,0,0,.3)` | 2.12:1 | 2.90:1 | 3.50:1 |
| `rgba(0,0,0,.4)` | 2.85:1 | 3.83:1 | 4.55:1 |
| `rgba(0,0,0,.45)` | 3.36:1 | 4.47:1 | 5.29:1 |
| `rgba(0,0,0,.5)` | 4.00:1 | 5.26:1 | 6.11:1 |
| `rgba(0,0,0,.55)` | **4.74:1** | 6.13:1 | 7.07:1 |
| `rgba(0,0,0,.6)` | 5.74:1 | 7.28:1 | 8.32:1 |

Read the white column as the worst case: with **55% black**, white text passes 4.5:1 over any photo, even a blown-out sky; **45%** is enough for large text. A 30% overlay — common in hero banners because it "looks nice" — leaves white text at 2.12:1 on the light parts.

You rarely need the overlay everywhere. A **gradient** that is dark only where the text sits keeps the rest of the photo bright:

```css
.card-text {
  background: linear-gradient(to top, rgba(0,0,0,.75), rgba(0,0,0,.55) 60%, transparent);
  padding-top: 4rem;   /* room for the fade above the first line */
}
```

The contrast rule applies to the part behind the letters: check that the gradient is still at least 55% where the top line of text starts, not only at the bottom edge. With this CSS over a pure white photo, the pixel at the top of the first line measured 5.66:1 against white text.

**Other tools, and their limits:**

- **`text-shadow`** (`0 1px 3px rgba(0,0,0,.6)`) softens edges but darkens only a few pixels around each letter; on a light photo the letters still sit on light pixels. Use it with an overlay, not instead.
- **A solid box** behind the text (`background: rgba(0,0,0,.7)` on the caption itself, with padding) is the most robust and works with any photo.
- **Dark text** on a light overlay (`rgba(255,255,255,.8)`) works the same way in reverse.
- **`mix-blend-mode`** and `backdrop-filter: blur()` look striking, but their contrast depends on the photo and cannot be guaranteed.

## Responsive details

- Give the container a size: `aspect-ratio`, `min-height` or a fixed height. An `<img>` with `height: 100%` inside a container without a height collapses to its natural height.
- Scale the text with the box using `clamp()` — `font-size: clamp(1.25rem, 4vw, 2.5rem)` — so a headline does not cover the whole photo on a phone.
- Keep the text in HTML, not baked into the image file: it stays selectable, translatable and readable by screen readers and search engines.

## When you need the result as an image

Everything above is for a web page, where the browser draws the text over the photo each time. Social posts, link previews, email headers and print need a **single image file** with the text already in it. That is what Shareglint templates do: the same HTML and CSS, drawn by Chrome on our servers, with the text and the photo as fields.

The built-in `photo-feature` template, for example, puts a title, credit and caption over a photo with a bottom gradient that reaches about 92% black behind the text:

```bash
curl -X POST "https://shareglint.com/v1/images?wait=true" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template": "photo-feature",
    "size": "og",
    "modifications": {
      "photo": "https://example.com/forest.jpg",
      "title": "Morning light in the old beech forest",
      "credit": "Ines Varga / Northwind Photo"
    }
  }'
```

The response's `url` is the finished image. Your own templates can use any of the three layouts on this page; fonts come from our installed list ([fonts and emoji](https://shareglint.com/en/guides/fonts-and-emoji-in-generated-images)). The free template preview shows the result before you use any quota, and the [template gallery](https://shareglint.com/en/templates) has other text-over-photo layouts — `meme-caption`, `real-estate`, `thumbnail-face` — to start from.

## FAQ

### How do I put text over an image in CSS?

Wrap the image and the text in one container with position: relative, make the image fill it with width: 100%, height: 100% and object-fit: cover, and position the text with position: absolute. A CSS grid with both children in the same cell does the same without absolute positioning.

### How dark should the overlay behind white text be?

To keep white text at WCAG's 4.5:1 even over a pure white part of the photo, the black overlay needs about 55% opacity; for large text (3:1) about 45%. We measured 4.74:1 at rgba(0,0,0,.55) and 4.00:1 at .5 over white in Chrome.

### Is text-shadow enough to make text readable on a photo?

Not on its own over a busy or light photo: it darkens only a few pixels around each letter and does not change the contrast of the text against the photo. Use it together with an overlay or a gradient, not instead of one.

### Should I use an img element or a background image?

Use an img when the picture is content — it gets alt text and loads like any image. Use background-image when it is decoration. Both can be cropped to fill the box: object-fit: cover for img, background-size: cover for backgrounds.
