What Is a CSS Overlay

A CSS overlay is when you place something “on top of” something else—usually to tint an image, dim a background, highlight a region, or show text on top of media.

There’s no single overlay: property in CSS. Instead, overlays are built using a few reliable patterns:

  • Pseudo-element overlays (most common): ::before / ::after positioned over a box.
  • Extra overlay element: a child <div class="overlay"> that sits on top of content.
  • Background-layer overlays: multiple backgrounds like linear-gradient(...), url(...).
  • Filter overlays: blur or tint the background under a panel using backdrop-filter.

In this tutorial, we’ll build overlays that are readable, clickable, and not secretly broken by stacking contexts (CSS has a talent for that).

CSS Overlay Color Basics

The classic overlay is a semi-transparent layer of color on top of a box. The most flexible approach is a pseudo-element:

  • position: relative on the parent so the overlay can anchor to it
  • position: absolute and inset: 0 on the pseudo-element so it fills the parent
  • background + opacity (or an rgba() / hsl(... / ...) color)
.card::before {
  opacity: 0.15;
}
  
.card::before {
  opacity: 0.55;
}
  
.card::before {
  background: #0077b6;
  opacity: 0.45;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.card {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  padding: 18px;
  background: #fff;
  box-shadow: 0 12px 0 #111;
  overflow: hidden;
  display: grid;
  gap: 10px;
  min-height: 220px;
}

.card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: #ef233c;
}

.card > * {
  position: relative;
  z-index: 1;
}

.badge {
  width: fit-content;
  padding: 6px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #fff;
  font-size: 14px;
  font-weight: 650;
}

.card p {
  margin: 0;
  max-width: 52ch;
  line-height: 1.45;
}
  
Overlay: pseudo-element

Readable content stays above the overlay

The overlay is on ::before, while the content is lifted with z-index: 1.

Opacity vs RGBA: a common beginner gotcha

If you put opacity: 0.5; on the parent, you’ll fade everything inside it (text, buttons, your hopes). That’s why we fade only the overlay layer (the pseudo-element).

CSS Overlay Color on Image

When you want a colored overlay on top of an image, you have two common approaches:

  • Use an <img> and add a pseudo-element overlay on the container (great when the image is content).
  • Use layered backgrounds: linear-gradient(...), url(...) (great when the image is purely decorative).

Here’s the container + <img> pattern with a gradient overlay:

.hero {
  isolation: isolate;
}
.hero::before {
  background: rgba(0, 0, 0, 0.55);
}
.hero img {
  z-index: -1;
}
  
.hero {
  isolation: isolate;
}
.hero::before {
  background: linear-gradient(90deg, rgba(239, 35, 60, 0.55), rgba(0, 119, 182, 0.55));
}
.hero img {
  z-index: -1;
}
  
.hero {
  isolation: isolate;
}
.hero::before {
  background: radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.75));
}
.hero img {
  z-index: -1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.hero {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  min-height: 280px;
  display: grid;
  align-content: end;
  padding: 18px;
}

.hero img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
}

.hero > * {
  position: relative;
  z-index: 1;
  color: #fff;
}

.hero h3 {
  margin: 0 0 6px;
  line-height: 1.1;
}

.hero p {
  margin: 0;
  max-width: 55ch;
  line-height: 1.45;
}

.pill {
  width: fit-content;
  margin-bottom: 10px;
  padding: 6px 10px;
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.15);
  font-size: 13px;
  font-weight: 650;
}
  
Random scenic photo
Overlay on an image

Make text readable on photos

The overlay is a separate layer. Your image stays crisp, your text stays readable.

Accessibility note

If text sits on an image, your overlay isn’t just decoration: it’s often the difference between readable and “why is this gray on gray?”. Aim for good contrast and consider checking with a contrast tool.

CSS Overlay Image and Text on Image

Sometimes you want an overlay that is itself an image (like a texture, pattern, or watermark) and still keep text on top. We’ll stack layers like this:

  1. Base image (<img>)
  2. Overlay image (pseudo-element with background-image)
  3. Optional tint (another pseudo-element)
  4. Text content on top

You can do this with two pseudo-elements: ::before for the texture and ::after for the tint.

.banner::before {
  opacity: 0.18;
}
.banner::after {
  opacity: 0.25;
}
  
.banner::before {
  opacity: 0.35;
}
.banner::after {
  opacity: 0.55;
}
  
.banner::before {
  mix-blend-mode: overlay;
  opacity: 0.55;
}
.banner::after {
  opacity: 0.35;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.banner {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  min-height: 300px;
  padding: 18px;
  display: grid;
  align-content: end;
  isolation: isolate;
}

.banner img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  transform: scale(1.02);
}

.banner::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: url("https://picsum.photos/900/600");
  background-size: cover;
  background-position: center;
  opacity: 0.18;
  pointer-events: none;
}

.banner::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0.25;
  pointer-events: none;
}

.banner > * {
  position: relative;
  z-index: 3;
  color: #fff;
}

.banner h3 {
  margin: 0 0 6px;
}

.banner p {
  margin: 0;
  max-width: 56ch;
  line-height: 1.45;
}

.kicker {
  width: fit-content;
  margin-bottom: 10px;
  padding: 6px 10px;
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.2);
  font-size: 13px;
  font-weight: 650;
}
  

See the entire CSS needed here by switching to the "Default CSS" tab in the playground.

Layer order and z-index

A simple rule: background/image layers go first, overlays go next, content goes last. In CSS terms, that usually means: pseudo-elements are absolutely positioned, and content gets position: relative + z-index: 3.

CSS Overlay Effects

“Effects” often means your overlay changes on hover, reveals something, or blends in a dramatic way. Let’s build a hover-reveal overlay that:

  • dims the image by default
  • reveals more brightness on hover
  • slides a caption up
.tile::before {
  opacity: 0.65;
}
.tile .caption {
  transform: translateY(0);
  opacity: 1;
}
  
.tile:hover::before {
  opacity: 0.25;
}
.tile .caption {
  transform: translateY(18px);
  opacity: 0.2;
}
.tile:hover .caption {
  transform: translateY(0);
  opacity: 1;
}
  
.tile:hover::before {
  opacity: 0.1;
}
.tile:hover .caption {
  transform: translateY(-6px);
  opacity: 1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.tile {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  width: min(640px, 100%);
  min-height: 310px;
  display: grid;
  align-content: end;
  padding: 18px;
  isolation: isolate;
}

.tile img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  transform: scale(1.05);
}

.tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0.65;
  transition: opacity 220ms ease;
  pointer-events: none;
}

.tile > * {
  position: relative;
  z-index: 2;
  color: #fff;
}

.caption {
  display: grid;
  gap: 8px;
  transition: transform 220ms ease, opacity 220ms ease;
  transform: translateY(0);
  opacity: 1;
}

.caption h3 {
  margin: 0;
}

.caption p {
  margin: 0;
  max-width: 55ch;
  line-height: 1.45;
}

.hint {
  width: fit-content;
  padding: 6px 10px;
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.2);
  font-size: 13px;
  font-weight: 650;
}
  
Random photo
Hover me

Overlay hover reveal

The overlay fades away on hover. The caption animates for extra polish.

Tip: when overlays block clicks

If you ever add buttons or links on top of an overlay and clicks “don’t work”, your overlay might be intercepting pointer events. Fix with pointer-events: none; on the overlay layer (usually the pseudo-element).

CSS Overlay Filter

Filters are a fun way to style what’s behind (or the overlay itself), but there are two different concepts:

  • filter affects the element itself (and its children). Put it on the image layer if you want the image blurred or desaturated.
  • backdrop-filter affects whatever is behind a semi-transparent element. This is the “frosted glass” overlay effect.

Let’s compare them with a card overlay panel.

.photo {
  filter: blur(0px) saturate(1);
}
.panel {
  backdrop-filter: blur(0px);
  background: rgba(255, 255, 255, 0.25);
}
  
.photo {
  filter: blur(3px) saturate(1.2);
}
.panel {
  backdrop-filter: blur(0px);
  background: rgba(255, 255, 255, 0.25);
}
  
.photo {
  filter: blur(0px) saturate(1);
}
.panel {
  backdrop-filter: blur(10px);
  background: rgba(255, 255, 255, 0.28);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.scene {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  min-height: 310px;
}

.photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1.05);
  filter: blur(0px) saturate(1);
}

.scene::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.25);
}

.panel {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 16px;
  padding: 14px;
  color: #fff;
  background: rgba(255, 255, 255, 0.25);
  backdrop-filter: blur(0px);
}

.panel h3 {
  margin: 0 0 6px;
  color: #111;
  background: rgba(255, 255, 255, 0.85);
  width: fit-content;
  padding: 4px 8px;
  border-radius: 10px;
}

.panel p {
  margin: 0;
  max-width: 60ch;
  line-height: 1.45;
  color: #111;
  background: rgba(255, 255, 255, 0.85);
  width: fit-content;
  padding: 6px 8px;
  border-radius: 10px;
}
  
Random texture

Filter vs backdrop-filter

filter changes the image itself. backdrop-filter blurs what’s behind the panel.

Browser note

backdrop-filter is widely supported in modern browsers, but not universally in older ones. Always make sure your panel stays readable even without the blur (the background: rgba(...) is your fallback).

CSS Overlay Div

Pseudo-elements are great, but sometimes you want the overlay to be a real element because:

  • it contains interactive UI (buttons, a close icon, a form)
  • you want to toggle it with JS later
  • you want it to be easier to understand in HTML

Here’s a simple overlay div pattern: a container with an image, plus a positioned overlay panel.

Switch to the HTML tab to see the structure here.

.overlay {
  inset: auto 16px 16px 16px;
  opacity: 1;
}
  
.overlay {
  inset: 0;
  opacity: 1;
}
  
.overlay {
  inset: 0;
  opacity: 0;
}
.stage:hover .overlay {
  opacity: 1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.stage {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  width: min(700px, 100%);
  min-height: 310px;
}

.stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.overlay {
  position: absolute;
  inset: auto 16px 16px 16px;
  border-radius: 16px;
  padding: 14px;
  background: rgba(255, 255, 255, 0.88);
  border: 2px solid #111;
  opacity: 1;
  transition: opacity 200ms ease;
}

.overlay h3 {
  margin: 0 0 6px;
}

.overlay p {
  margin: 0;
  max-width: 60ch;
  line-height: 1.45;
}

.small {
  margin-top: 10px;
  font-size: 14px;
  opacity: 0.8;
}
  
Random photo

Overlay as a real element

This overlay can contain UI. It can also be revealed on hover, click, or with JS.

Tip: if it contains buttons, don’t use a pseudo-element.

CSS Overlay Scrollbar

“Overlay scrollbar” can mean two things:

  • A scrollbar inside an overlay panel (a scrollable overlay).
  • A scrollbar style that looks like it floats over content (browser UI territory, only partially controllable in CSS).

The beginner-friendly, reliable part is the first one: a scrollable overlay panel. We’ll create an overlay that sits on the right and scrolls inside.

.drawer {
  width: min(360px, 75%);
}
  
.drawer {
  width: min(420px, 85%);
}
  
.drawer {
  width: min(320px, 70%);
}
.drawer {
  background: rgba(255, 255, 255, 0.92);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.panel-scene {
  position: relative;
  border: 3px solid #111;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 12px 0 #111;
  width: min(860px, 100%);
  min-height: 360px;
}

.panel-scene img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.dim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
}

.drawer {
  position: absolute;
  top: 12px;
  bottom: 12px;
  right: 12px;
  width: min(360px, 75%);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.88);
  border: 2px solid #111;
  padding: 12px;
  overflow: auto;
}

.drawer h3 {
  margin: 0 0 10px;
}

.drawer p {
  margin: 0 0 10px;
  line-height: 1.45;
}

.drawer .block {
  border: 2px solid #111;
  border-radius: 14px;
  padding: 10px;
  background: #fff;
  margin-bottom: 10px;
}

.drawer code {
  background: #f2f2f2;
  padding: 2px 6px;
  border-radius: 8px;
}

.drawer::-webkit-scrollbar {
  width: 12px;
}

.drawer::-webkit-scrollbar-thumb {
  background: rgba(17, 17, 17, 0.45);
  border-radius: 999px;
  border: 3px solid rgba(255, 255, 255, 0.75);
}
  
Random background

Realistic expectations for scrollbars

Scrollbar styling is not fully standardized across all browsers. The most portable technique is simply: make your overlay panel scroll with overflow: auto, and keep it usable even with default scrollbar styles.

Common Overlay Pitfalls

1) Forgetting position: relative

If your overlay is position: absolute but your parent isn’t positioned, the overlay may attach to the wrong ancestor (often the page). Put position: relative on the container you want to cover.

2) Content hidden behind the overlay

If text looks “muted” or unclickable, make sure content is above the overlay layer: give the content position: relative and z-index: 3.

3) Overlay blocks clicks

If you’re using a pseudo-element overlay and it sits above your buttons, add: pointer-events: none; to the overlay pseudo-element.

4) z-index “does nothing”

z-index only works on positioned elements (position not static). Also, parents can create their own stacking contexts (for example with transform, filter, or opacity). If layering looks wrong, check whether a parent is creating a new stacking context. Learn more in the CSS Z-Index Interactive Tutorial.

CSS Overlay Wrap-Up

You now have a full overlay toolkit:

  • Color overlays using pseudo-elements (clean and flexible)
  • Image overlays and text on images with readable layering
  • Overlay effects like hover reveals and blends
  • Overlay filters with filter and backdrop-filter
  • Overlay panels with scrollable content and optional scrollbar styling

With these techniques, you can create versatile and visually appealing overlays for various UI patterns.

Further Reading