Home / Snippets / Color & Theming /
Image overlay background
Stack a semi-transparent linear-gradient on top of a background image (or texture) to guarantee readable text without wrapping the image in extra markup.
Quick implementation
.hero {
background:
/* 1. Overlay: semi-transparent color on top */
linear-gradient(
oklch(0.15 0.02 260 / 0.85),
oklch(0.15 0.02 260 / 0.85)
),
/* 2. Your background image (or texture) below */
url('/images/hero-photo.jpg') center / cover no-repeat;
}
/* Vary overlay intensity with the alpha channel */
.hero--light-overlay {
background:
linear-gradient(
oklch(0.15 0.02 260 / 0.55),
oklch(0.15 0.02 260 / 0.55)
),
url('/images/hero-photo.jpg') center / cover no-repeat;
}
/* Directional fade: dark on left for side-by-side layouts */
.hero--directional {
background:
linear-gradient(
to right,
oklch(0.15 0.02 260 / 0.92) 0%,
oklch(0.15 0.02 260 / 0.0) 60%
),
url('/images/hero-photo.jpg') center / cover no-repeat;
}
Prompt this to your LLM
Includes role, constraints, two layout variants, and edge cases to handle.
You are a senior frontend engineer building a hero section with a
photo background and overlaid text that must meet WCAG AA contrast.
Goal: Use a CSS gradient overlay stacked on a background image so
that text remains readable without adding any extra HTML elements
or JavaScript.
Technical constraints:
- Use the CSS background shorthand to stack multiple background
layers: the overlay gradient comes first (topmost layer), the
image URL comes last (bottommost layer).
- The overlay is a solid-color linear-gradient with an oklch() color
at a specific alpha value, e.g.:
linear-gradient(
oklch(0.15 0.02 260 / 0.85),
oklch(0.15 0.02 260 / 0.85)
)
- Use oklch() for all color values — no hex or rgba.
- Do NOT use ::before or ::after pseudo-elements for the overlay;
the stacked background approach is more performant.
- The text inside the hero must be position: relative (or the hero
itself must establish a stacking context) so it appears above
the background layers.
Layout variant (pick one):
A) Full-width hero: uniform overlay across the entire background.
B) Split hero: directional gradient that fades from opaque on the
left (text side) to transparent on the right (image side), using
linear-gradient(to right, oklch(... / 0.9) 0%, oklch(... / 0) 55%).
Edge cases to handle:
- If the image fails to load, the gradient alone should still
provide a usable background (set a fallback background-color
on the element).
- Adjust overlay alpha to meet contrast: body text typically needs
at least 0.7 opacity on a mid-toned photo; display headings may
need 0.85+.
- Avoid animating background-color on an element that also has
background-image — composite the color change separately or
use opacity on a child element to prevent triggering repaints.
Return CSS only, including the fallback background-color.
Why gradient overlay beats pseudo-elements
The classic approach to overlaying color on a background image is a ::before pseudo-element set to position: absolute; inset: 0 with a semi-transparent background. It works, but it introduces a new stacking context, requires the parent to be position: relative, and means the overlay participates in paint as a separate layer. The stacked-background approach has none of these costs.
CSS allows multiple values in the background shorthand, separated by commas. Layers are painted bottom-to-top: the last value is the furthest back. Placing a flat linear-gradient as the first value puts it directly on top of the image. The browser composites everything into a single background paint operation — no extra DOM node, no new stacking context, no additional repaint surface.
The "flat" gradient trick — giving both color stops the same color — turns linear-gradient into a solid fill. A single-color background-color cannot layer on top of background-image because there is only one background-color slot per element and it always paints below the image layers. The gradient workaround is the correct solution.
oklch alpha for precise overlay intensity
Using oklch() for the overlay color has two advantages over rgba(). First, the lightness channel in oklch is perceptually uniform, so oklch(0.15 0.02 260) is a reliably dark, near-neutral color regardless of the hue. Second, the alpha channel maps directly to overlay strength: 0 is invisible, 1 is fully opaque. Because oklch lightness is linear to human perception, you can reason about contrast mathematically — an overlay of / 0.85 on a bright photo will darken it predictably, without the non-linear surprises you get from mixing RGB values.
A practical calibration: start at / 0.6 for lightly textured backgrounds, / 0.75 for colorful photography, and / 0.85–0.9 for high-contrast scenes like snowscapes or bright skies. For WCAG AA compliance, the text-to-effective-background contrast ratio must be at least 4.5:1 for body text and 3:1 for large headings. Crank the overlay up until the underlying image contributes less than ~30% to the perceived color beneath the text.
A directional overlay — fading from opaque on the text side to transparent on the image side — preserves visual interest in the image while still protecting contrast where it matters. Use linear-gradient(to right, oklch(0.15 0.02 260 / 0.9) 0%, oklch(0.15 0.02 260 / 0) 55%) to keep the right half of a hero image fully visible.
Ensuring text contrast
The overlay controls the background's effective lightness, but the text color must also be chosen deliberately. On a dark overlay (oklch(0.15 0.02 260) at 85% opacity), set body text to at least oklch(0.82 0.01 260) and headings to oklch(0.96 0.01 260) for comfortable reading. Avoid pure white (oklch(1 0 0)) — the slight desaturation from a hue-shifted overlay makes near-white feel softer and more intentional.
An accent color for eyebrow text or call-to-action links adds hierarchy without reducing contrast. A chroma-boosted value like oklch(0.78 0.16 265) sits perceptually between the dark overlay and the light headline text, guiding the eye naturally through the content. Because oklch hues are stable across lightness levels, you can freely adjust L to hit contrast targets without the color appearing to shift.
Always verify final contrast with a tool like the APCA Contrast Calculator. The overlay gives you a consistent, measurable starting point, but the actual contrast depends on the specific image content beneath the semi-transparent layer. If the image has areas significantly brighter than your tested average, increase the overlay alpha by 0.05–0.1 for those regions, or use a more opaque local gradient over the text block only.