CSS opacity: how see-through works, and when to use something else

opacity fades an element and everything inside it as one picture. That is right for fades and hidden captions, and wrong when you only wanted a see-through background.

CSS opacity sets how see-through an element is, from 0 (invisible) to 1 (solid). opacity: 0.5 makes it half transparent. It applies to the whole element: background, border, text and children fade together.

To fade only the background, use a color with an alpha value instead.

Move the slider. Both cards get the same number, applied two different ways.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>opacity vs background alpha</title>
<style>
  body {
    margin: 0; padding: 14px; box-sizing: border-box; min-height: 100vh;
    font-family: system-ui, sans-serif; color: #1d2330;
    /* a striped backdrop so you can see what shows through */
    background: repeating-linear-gradient(45deg, #2563eb 0 14px, #f59e0b 14px 28px);
  }
  .controls {
    background: #fff; border-radius: 10px; padding: 10px 12px; margin-bottom: 14px;
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14px;
  }
  .controls input { flex: 1; min-width: 140px; }
  .row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
  .card { border-radius: 12px; padding: 14px 16px; }
  .card h3 { margin: 0 0 6px; font-size: 16px; }
  .card p { margin: 0 0 8px; font-size: 14px; line-height: 1.4; }
  .card code { font-size: 13px; background: #eef1f5; padding: 1px 4px; border-radius: 4px; }

  /* A: opacity fades the whole card, text and children included */
  #a { background: #fff; opacity: var(--v); }

  /* B: only the background color is see-through; the text stays solid */
  #b { background: rgb(255 255 255 / var(--v)); }
</style>
</head>
<body style="--v: 0.5">
<div class="controls">
  <label for="v">Transparency</label>
  <input id="v" type="range" min="0" max="1" step="0.05" value="0.5">
  <output id="out">0.5</output>
</div>
<div class="row">
  <div class="card" id="a">
    <h3>opacity</h3>
    <p>The text fades with the card.</p>
    <code id="ca">opacity: 0.5</code>
  </div>
  <div class="card" id="b">
    <h3>background alpha</h3>
    <p>Only the white fill fades.</p>
    <code id="cb">rgb(255 255 255 / 0.5)</code>
  </div>
</div>

<script>
  const slider = document.getElementById('v');
  slider.addEventListener('input', () => {
    const v = slider.value;
    document.body.style.setProperty('--v', v);  // both cards read the same value
    document.getElementById('out').textContent = v;
    document.getElementById('ca').textContent = 'opacity: ' + v;
    document.getElementById('cb').textContent = 'rgb(255 255 255 / ' + v + ')';
  });
</script>
</body>
</html>
One slider, two cards. The left card uses opacity, the right one uses an alpha value in its background color.

Values: 0 to 1, or a percentage

opacity takes one number. 0 is fully transparent, 1 is fully opaque, and anything between is partly see-through. A percentage works too, so opacity: 50% equals opacity: 0.5.

.faded  { opacity: 0.5; }
.ghost  { opacity: 20%; }
.solid  { opacity: 1; }   /* the default */

Values outside the range are clamped: 1.5 acts like 1, and -1 acts like 0. HTML has no opacity attribute for elements, so inline you write style="opacity: 0.5".

Why the text fades too

The browser paints the element and all its children into one picture, then fades that picture. That is why the card's heading turns see-through along with its white fill.

opacity fades the card as a whole. An alpha color fades only the fill behind the text.
opacity fades the card as a whole. An alpha color fades only the fill behind the text.

When the goal is a see-through panel over a photo with readable text, move the transparency into the color:

.panel { background: rgb(255 255 255 / 0.6); }   /* same as rgba(255, 255, 255, 0.6) */
.dark  { background: hsl(220 30% 10% / 60%); }
.hex   { background: #ffffff99; }                /* last two digits are the alpha */
.mix   { background: color-mix(in srgb, white 60%, transparent); }

All four fade only the background. The same idea works for borders, text and shadows: give that one color an alpha value. This is also the answer to "background opacity in CSS", since no separate background-opacity property exists.

opacity: 0 vs visibility: hidden vs display: none

All three make an element disappear from view. They differ in what is left behind. Hide the three buttons below, then click where they were and press Tab from the Start here button.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>opacity: 0 vs visibility: hidden vs display: none</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; color: #1d2330; background: #f4f5f7; }
  .bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
  .bar button { font: inherit; font-size: 14px; padding: 8px 12px; border-radius: 8px; border: 1px solid #c9cdd4; background: #fff; cursor: pointer; }
  .row {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    background: #fff; border-radius: 10px; padding: 10px 12px; margin-bottom: 8px;
  }
  .row code { width: 150px; font-size: 13px; font-weight: 600; }
  .t { font: inherit; font-size: 14px; padding: 7px 12px; border-radius: 8px; border: 0; background: #2563eb; color: #fff; cursor: pointer; }
  .next { font-size: 13px; padding: 6px 10px; border-radius: 6px; background: #e8ebf0; }
  .status { flex-basis: 100%; font-size: 13px; color: #5b6270; min-height: 1.2em; }

  /* the three ways to hide, switched on by the "hidden" class on body */
  .hidden #op  { opacity: 0; }
  .hidden #vis { visibility: hidden; }
  .hidden #dis { display: none; }
</style>
</head>
<body>
<div class="bar">
  <button id="toggle">Hide the blue buttons</button>
  <button id="start">Start here, then press Tab</button>
</div>

<div class="row" data-name="opacity: 0"><code>opacity: 0</code><button class="t" id="op">Click me</button><span class="next">next item</span><div class="status"></div></div>
<div class="row" data-name="visibility: hidden"><code>visibility: hidden</code><button class="t" id="vis">Click me</button><span class="next">next item</span><div class="status"></div></div>
<div class="row" data-name="display: none"><code>display: none</code><button class="t" id="dis">Click me</button><span class="next">next item</span><div class="status"></div></div>

<script>
  const toggle = document.getElementById('toggle');
  toggle.addEventListener('click', () => {
    const on = document.body.classList.toggle('hidden');
    toggle.textContent = on ? 'Show the blue buttons' : 'Hide the blue buttons';
    document.querySelectorAll('.status').forEach((s) => (s.textContent = on
      ? 'Hidden. Click where the button was, or press Tab.' : ''));
  });

  document.querySelectorAll('.row').forEach((row) => {
    const status = row.querySelector('.status');
    const btn = row.querySelector('.t');
    // a click that reaches the button
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      status.textContent = 'The button got the click.';
    });
    // a click anywhere else in the row, including the empty spot
    row.addEventListener('click', () => {
      status.textContent = 'The click hit the empty row. The button did not get it.';
    });
    btn.addEventListener('focus', () => {
      status.textContent = 'Tab stopped on this button.';
    });
  });
</script>
</body>
</html>
Hide the buttons, then click the empty spots and press Tab. The row reports what the click or Tab reached.
opacity: 0 visibility: hidden display: none
Keeps its space Yes Yes No
Gets clicks and taps Yes No No
Tab key stops on it Yes No No
Screen readers reach it Yes No No
Can be faded with transition Yes Only as an on/off step No
Only opacity: 0 leaves a live, invisible button in the page.
Only opacity: 0 leaves a live, invisible button in the page.

Use opacity: 0 for the start or end of a fade. Use visibility: hidden when the space should stay but nothing should respond. Use display: none when the element should be gone from the page entirely.

Fading in and out with transition

opacity animates smoothly between any two values. Put the transition on the element's normal state, and change the value on hover or with a class:

.toast {
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s, visibility .3s;
}
.toast.show {
  opacity: 1;
  visibility: visible;
}

Pairing visibility with opacity fixes the click problem. visibility switches in one step, and the browser keeps it visible for the whole transition, so the fade still plays both ways. CSS hover transition covers where the transition rule goes.

Opacity creates a stacking context

Any opacity below 1, even 0.99, makes the element a new stacking context. Its children are then layered only among themselves. A child's z-index: 999 can no longer lift it above something outside the parent.

With opacity on the parent, the child's z-index only counts inside the parent.
With opacity on the parent, the child's z-index only counts inside the parent.

If a dropdown or tooltip slides behind its neighbours after you add a fade, this is the reason. CSS z-index explains stacking contexts in full.

Image opacity on hover

Dimming a picture on hover is one line plus a transition. A dark background behind the image makes the dimmed version darker instead of washed out. The finished tile below fades the image down and a caption up.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Image caption that fades in on hover</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  .hint { margin: 0 0 10px; font-size: 14px; color: #5b6270; }
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }

  .tile {
    position: relative; margin: 0; border-radius: 12px; overflow: hidden;
    background: #111;  /* shows through when the image fades */
    aspect-ratio: 4 / 3;
  }
  .tile svg { display: block; width: 100%; height: 100%; transition: opacity .3s; }
  .tile figcaption {
    position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 12px;
    color: #fff; font-size: 14px; line-height: 1.35;
    background: rgb(0 0 0 / .55);  /* see-through fill, solid text */
    opacity: 0;                    /* hidden, but still read by screen readers */
    transition: opacity .3s;
  }
  .tile figcaption b { display: block; font-size: 15px; }

  /* mouse over the tile, or keyboard focus on it */
  .tile:hover figcaption, .tile:focus-visible figcaption { opacity: 1; }
  .tile:hover svg, .tile:focus-visible svg { opacity: .7; }
  .tile:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }

  /* touch screens have no hover: show the captions all the time */
  @media (hover: none) {
    .tile figcaption { opacity: 1; }
  }
  /* respect the "reduce motion" setting: switch instantly */
  @media (prefers-reduced-motion: reduce) {
    .tile svg, .tile figcaption { transition: none; }
  }
</style>
</head>
<body>
<p class="hint">Hover a picture, or press Tab to move between them.</p>
<div class="grid">
  <figure class="tile" tabindex="0">
    <svg viewBox="0 0 120 90" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <rect width="120" height="90" fill="#7dd3fc"/><circle cx="92" cy="22" r="10" fill="#fde047"/>
      <path d="M0 90 L38 40 L62 70 L84 48 L120 90Z" fill="#166534"/>
    </svg>
    <figcaption><b>Ridge</b>Morning light over the pass.</figcaption>
  </figure>
  <figure class="tile" tabindex="0">
    <svg viewBox="0 0 120 90" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <rect width="120" height="90" fill="#fb923c"/><circle cx="60" cy="58" r="20" fill="#fef08a"/>
      <rect y="62" width="120" height="28" fill="#1e3a8a"/>
    </svg>
    <figcaption><b>Harbour</b>Sunset from the pier.</figcaption>
  </figure>
  <figure class="tile" tabindex="0">
    <svg viewBox="0 0 120 90" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <rect width="120" height="90" fill="#bbf7d0"/>
      <rect x="18" y="30" width="14" height="60" fill="#78350f"/><circle cx="25" cy="30" r="18" fill="#15803d"/>
      <rect x="74" y="40" width="12" height="50" fill="#78350f"/><circle cx="80" cy="40" r="15" fill="#16a34a"/>
    </svg>
    <figcaption><b>Orchard</b>Two trees, late spring.</figcaption>
  </figure>
</div>
</body>
</html>
Hover a tile or Tab to it. The caption fades in; with "reduce motion" on it switches instantly, and on touch screens it is always shown.
  • Hover and keyboard: the same rule lists :hover and :focus-visible, and each tile has tabindex="0".
  • Caption fill: rgb(0 0 0 / .55) behind the caption, so the fill is see-through but the white text stays solid.
  • Touch screens: @media (hover: none) shows captions all the time, since a finger cannot hover.
  • Reduced motion: @media (prefers-reduced-motion: reduce) turns the transition off.

A caption at opacity: 0 is still read by screen readers, which suits a gallery. For a full grid, see HTML image gallery.

When it does not work

What you see Cause Fix
The text fades along with the background opacity fades the element and all its children Use an alpha color on background instead
An invisible element still catches clicks opacity: 0 keeps it clickable and focusable Add visibility: hidden or pointer-events: none
A child stays faded even with opacity: 1 The parent is faded as one group Move the child out of the parent, or fade only the parent's background
The fade does not play when showing the element It starts from display: none, which does not transition Hide with opacity: 0 plus visibility: hidden
A dropdown slides behind other content after adding a fade Opacity below 1 creates a stacking context Put the z-index on the faded parent, or fade a different element

For the display: none case, newer CSS adds @starting-style and transition-behavior: allow-discrete to animate from it. The visibility pairing above works without them.

A fade is easier to show than to describe. A screenshot catches one frame, and an .html attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it. The page renders as written and its scripts run, so the people you send it to can drag the slider and hover the tiles themselves.

If you change the code later, the same link shows the new version.

Questions people ask

What values does CSS opacity take?

A number from 0 to 1, or a percentage from 0% to 100%. 0 is fully transparent, 1 is fully opaque, and 0.5 or 50% is half. Values outside the range are not errors; the browser clamps them to 0 or 1.

Is there an opacity attribute in HTML?

Not for HTML elements. Set it with CSS, either in a stylesheet or inline, for example <div style="opacity: 0.5">. SVG elements are different: they do accept opacity, fill-opacity and stroke-opacity as attributes.

How do I make only the background transparent in CSS?

Put the transparency in the background color instead of using opacity. background: rgb(255 255 255 / 0.5), rgba(255, 255, 255, 0.5), hsl(0 0% 100% / 50%) or an 8-digit hex such as #ffffff80 all give a half see-through white fill while the text stays solid.

Is an element with opacity: 0 still clickable?

Yes. It is invisible but still in the layout, still receives clicks, still takes keyboard focus, and screen readers can still reach it. Add pointer-events: none to stop clicks, or use visibility: hidden to take it out of clicks, focus and screen readers together.

Can a child element be more opaque than its parent?

No. The parent and its children are painted together and then faded as one group. Setting opacity: 1 on the child does nothing extra, because opacity is not inherited in the first place; the child is already at 1 inside a faded group.

Keep reading