CSS pointer-events: let clicks pass through

pointer-events: none makes an element invisible to the mouse while it stays visible on screen. Clicks, taps and hover go to whatever sits underneath it.

pointer-events: none tells the browser to ignore an element when it works out what the mouse is over. The element stays on screen, but clicks, taps and hover fall through to whatever is underneath. The default value, auto, is the normal behavior.

Try it below. The blue overlay covers both buttons. Click them, then tick the box and click again.

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>pointer-events: none lets clicks through</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .stage { position: relative; max-width: 420px; padding: 24px 16px; border-radius: 12px; background: #fff; }
  .stage button { font: inherit; padding: 10px 16px; margin: 4px; border-radius: 8px; border: 1px solid #c7ccd6; background: #fff; cursor: pointer; }
  .stage button:hover { background: #e8f0ff; }
  /* The overlay covers both buttons */
  .overlay {
    position: absolute; inset: 0; border-radius: 12px;
    background: rgba(37, 99, 235, .18); border: 2px dashed #2563eb;
    display: flex; align-items: flex-end; justify-content: flex-end;
    padding: 6px 10px; font-size: 12px; color: #1e40af;
  }
  .overlay.through { pointer-events: none; }  /* clicks go to what is underneath */
  label { display: block; margin: 14px 0 8px; font-weight: 600; }
  #log { margin: 0; padding: 10px 12px; min-height: 90px; border-radius: 8px; background: #1d2330; color: #d1fae5; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="stage">
  <button id="save">Save</button>
  <button id="share">Share</button>
  <div class="overlay" id="overlay">overlay</div>
</div>

<label><input type="checkbox" id="toggle"> pointer-events: none on the overlay</label>
<pre id="log">Click the buttons.</pre>

<script>
  const overlay = document.getElementById('overlay');
  const log = document.getElementById('log');
  const lines = [];

  document.getElementById('toggle').addEventListener('change', (e) => {
    overlay.classList.toggle('through', e.target.checked);
  });

  // Report which element actually received each click
  document.querySelector('.stage').addEventListener('click', (e) => {
    const name = e.target.id === 'overlay' ? 'the overlay' : 'button "' + e.target.textContent + '"';
    lines.unshift('click landed on ' + name);
    log.textContent = lines.slice(0, 4).join('\n');
  });
</script>
</body>
</html>
The log shows which element received each click. With the box ticked, the overlay is skipped.
.overlay { pointer-events: none; }

That one line is the whole fix for a see-through layer that blocks clicks. The rest of this guide is about where it helps, and where it quietly does less than it seems to.

How pointer-events: none works

Every time the pointer moves or presses, the browser looks for the topmost element at that point. That element becomes the event target. An element with pointer-events: none is left out of that search, so the next element down is chosen.

With auto, the overlay is the target. With none, the browser skips it and the button gets the click.
With auto, the overlay is the target. With none, the browser skips it and the button gets the click.

Only the target changes. The overlay still paints, still takes up space in the layout, and still sits above the button visually. If you need it gone, that is a job for display or visibility, not for this property.

Value Where to use it What it does
auto Anything Normal behavior. The default.
none Anything Never the target. Events go to what is underneath.
visiblePainted, fill, stroke, all and a few more SVG shapes Choose which part of a shape responds

Parent none, child auto

pointer-events is inherited. Put none on a container and every element inside it is none as well, so the whole block lets clicks through.

You can switch a single child back on with pointer-events: auto. That child takes clicks again, and the rest of the container stays see-through.

The children inherit none. The one reset to auto is clickable, and its click still bubbles to the parent.
The children inherit none. The one reset to auto is clickable, and its click still bubbles to the parent.
.toolbar { pointer-events: none; }
.toolbar .live { pointer-events: auto; }

The click on the child still bubbles up through the parent. A listener on the container hears it, even though the container itself can never be the target. The demo below shows this in its log.

It is not a real disabled state

A common use is a link that looks disabled: grey text and pointer-events: none. The mouse can no longer click it. The keyboard can.

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>Parent none, child auto</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h3 { font-size: 14px; margin: 0 0 8px; }
  .bar {
    pointer-events: none;           /* the bar ignores the mouse... */
    display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    padding: 10px; border-radius: 10px; background: #fff; border: 1px solid #dfe3ea;
  }
  .bar button { font: inherit; padding: 8px 12px; border-radius: 8px; border: 1px solid #c7ccd6; background: #fff; cursor: pointer; }
  .bar .live { pointer-events: auto; background: #d6f2df; border-color: #86c69b; }  /* ...but this child is reset */
  a.disabled { pointer-events: none; color: #9aa3af; }
  a:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
  .box { margin-top: 18px; padding: 12px; border-radius: 10px; background: #fff; border: 1px solid #dfe3ea; }
  #log { margin: 12px 0 0; padding: 10px 12px; min-height: 70px; border-radius: 8px; background: #1d2330; color: #d1fae5; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<h3>Menu bar: pointer-events: none, one child set back to auto</h3>
<div class="bar" id="bar">
  <button>Bold</button>
  <button>Italic</button>
  <button class="live">Insert</button>
</div>

<div class="box">
  <h3>A "disabled" link with pointer-events: none</h3>
  <a href="#pricing" class="disabled" id="link">See pricing</a>
  <p style="font-size:13px;margin:8px 0 0">Click it: nothing. Now press Tab until it is outlined, then press Enter.</p>
</div>
<pre id="log">Click the buttons, then try the link.</pre>

<script>
  const log = document.getElementById('log');
  const lines = [];
  const say = (t) => { lines.unshift(t); log.textContent = lines.slice(0, 4).join('\n'); };

  // The listener sits on the parent. It still hears clicks from the "auto" child.
  document.getElementById('bar').addEventListener('click', (e) => {
    say('bar heard a click from "' + e.target.textContent + '"');
  });

  document.getElementById('link').addEventListener('click', (e) => {
    e.preventDefault();  // stay on this page for the demo
    say('link activated (' + (e.detail === 0 ? 'keyboard or script' : 'mouse') + ')');
  });
</script>
</body>
</html>
Top: only Insert takes clicks, and the bar's listener hears it. Bottom: the link ignores the mouse, but Tab and Enter still open it.

pointer-events only affects hit testing, the step that decides which element is under the pointer. It does not remove anything from the tab order and does not block events that come from somewhere else.

What pointer-events: none stops, and what it leaves working.
What pointer-events: none stops, and what it leaves working.
  • Tab still moves focus to the link or button. tabindex controls that order.
  • Enter on a focused link, or Enter and Space on a focused button, still activate it.
  • element.click() in JavaScript still fires the click event and its listeners.
  • Screen readers still find the element, because it is still in the page.

To really disable something, use the right tool for the element:

<!-- A button: the disabled attribute stops mouse, keyboard and form submission -->
<button disabled>Save</button>

<!-- A link: without href it is not a link, so it is not focusable -->
<a aria-disabled="true">See pricing</a>

Keep pointer-events: none for elements that should never be interactive, such as decoration, rather than as an off switch.

Where it is the right tool

The property shines on layers that sit on top of something clickable but have nothing to do themselves.

  • Gradient or shade over an image link, so a click anywhere on the picture still opens it.
  • Captions and labels placed over a clickable area, such as text on a map.
  • Custom cursors and glow effects that follow the pointer. Without none, the dot under the pointer catches every click. CSS cursor builds one.
  • Watermarks and "Preview" stamps laid over a page.

Transparent elements need this too. An element at opacity: 0 is still clickable, which CSS opacity covers in detail.

SVG values in brief

SVG shapes have extra values, because a shape has a fill and a stroke that can be painted or not. On SVG shapes, the default auto behaves like visiblePainted: a shape responds only where its fill or stroke is actually painted and the shape is visible.

That is why a rectangle drawn with fill="none" ignores clicks in its middle. A fill of transparent counts as painted, so it does respond.

  • fill: responds over the fill area, even if the fill is none.
  • stroke: responds over the stroke only.
  • all: responds over fill or stroke, whatever the paint or visibility.
  • none: never responds. Useful for text labels on top of shapes.

SVG in HTML shows how to put a drawing like this into a page.

A finished example: image card and clickable map

This example puts both ideas together. On the left, a gradient and a caption cover an image link without blocking it. The Like button inside the caption is reset to auto. On the right, only the shapes on a map respond.

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>Card overlay and clickable map shapes</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .wrap { display: flex; flex-wrap: wrap; gap: 16px; }
  /* Image card: the link is the photo, the gradient and caption sit on top */
  .card { position: relative; width: 260px; height: 190px; border-radius: 14px; overflow: hidden; }
  .card a { display: block; height: 100%;
    background: radial-gradient(circle at 70% 30%, #fde68a 0 18%, transparent 19%),
                linear-gradient(180deg, #7dd3fc, #38bdf8 55%, #15803d 56%, #166534); }
  .card a:hover { filter: brightness(1.08); }
  .shade {
    position: absolute; inset: 0; display: flex; align-items: flex-end; justify-content: space-between;
    padding: 12px; color: #fff; background: linear-gradient(transparent 45%, rgba(0, 0, 0, .7));
    pointer-events: none;           /* the gradient and caption never block the link */
  }
  .shade b { font-size: 15px; }
  .shade button { pointer-events: auto; font: inherit; border: 0; border-radius: 99px; padding: 6px 12px; background: #fff; cursor: pointer; }
  /* Map: only the painted shapes respond */
  svg { width: 260px; height: 190px; background: #fff; border-radius: 14px; }
  .region { fill: #cbd5e1; stroke: #64748b; stroke-width: 2; cursor: pointer; }
  .region:hover { fill: #93c5fd; }
  .region.outline { fill: none; pointer-events: fill; }  /* no paint inside, but the inside still counts */
  .region.outline:hover { stroke: #2563eb; stroke-width: 4; }
  svg text { font-size: 13px; fill: #1d2330; pointer-events: none; }  /* labels do not steal clicks */
  #out { margin-top: 14px; padding: 10px 12px; border-radius: 8px; background: #1d2330; color: #d1fae5; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="wrap">
  <div class="card">
    <a href="#photo-1" id="photo" aria-label="Open the photo"></a>
    <div class="shade"><b>Lake at noon</b><button id="like">Like</button></div>
  </div>

  <svg viewBox="0 0 260 190" id="map" aria-label="Zone map">
    <path class="region" data-name="North" d="M20 20 H150 V85 H20 Z"/>
    <path class="region" data-name="East" d="M160 20 H240 V170 H160 Z"/>
    <path class="region outline" data-name="South (outline only)" d="M20 95 H150 V170 H20 Z"/>
    <text x="62" y="57">North</text>
    <text x="182" y="100">East</text>
    <text x="62" y="137">South</text>
  </svg>
</div>
<div id="out">Click the photo, its caption, the Like button, or a zone.</div>

<script>
  const out = document.getElementById('out');

  document.getElementById('photo').addEventListener('click', (e) => {
    e.preventDefault();  // a real page would open the photo
    out.textContent = 'Opened the photo';
  });
  document.getElementById('like').addEventListener('click', () => {
    out.textContent = 'Liked';
  });

  // One listener for the map; e.target is the shape under the pointer
  document.getElementById('map').addEventListener('click', (e) => {
    out.textContent = e.target.dataset.name ? 'Zone: ' + e.target.dataset.name : 'Empty map area';
  });
</script>
</body>
</html>
Click the caption and the photo still opens. On the map, labels let clicks through and the outlined South zone uses pointer-events: fill.
  • Shade: the .shade layer has pointer-events: none, so its gradient and caption text never become the target.
  • Like button: pointer-events: auto brings it back inside the see-through shade.
  • Map labels: text { pointer-events: none; } so a click on the word "North" lands on the North shape.
  • Outline-only zone: fill: none plus pointer-events: fill makes the empty middle clickable.

When it does not work

What you see Cause Fix
The element still works from the keyboard pointer-events only stops the mouse Use disabled, remove href, or tabindex="-1"
A child cannot be clicked The parent has none and the child inherits it Set pointer-events: auto on the child
The overlay still blocks clicks The rule is on the wrong element, or another layer is on top Inspect the spot and put none on the element that is selected
A sibling above still catches clicks A higher z-index element covers the target Lower it or give it none. See z-index
:hover styles on the element never fire It is never the hovered element Put the hover on the element underneath, or on a child with auto
Its cursor never shows The cursor comes from the element underneath Set cursor on the element below
The text can still be selected Selection is not controlled by pointer-events Add user-select: none

For a button that will not respond at all, and how to find what is covering it, see HTML button not clickable. For text that should not highlight, see make text not highlightable.

Click-through behavior is hard to show in a screenshot. The person you send it to needs to move the mouse and click to see which layer answers.

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 people can click through the overlays themselves. If you change the code later, the same link shows the new version.

Questions people ask

What does pointer-events: none do?

The element is never the target of mouse, pen or touch events. When you click where it is, the browser picks the element underneath instead. The element still shows, and its :hover styles and its own cursor no longer apply.

Is pointer-events: none the same as disabling a link or button?

No. It only stops the mouse. The element can still be reached with Tab, activated with Enter, and clicked from JavaScript with element.click(). For a button, use the disabled attribute. For a link that should not work, remove its href.

Can a child be clickable if its parent has pointer-events: none?

Yes. pointer-events is inherited, so the children are none too unless you say otherwise. Set pointer-events: auto on the child and it takes clicks again. The click then bubbles up through the parent as usual.

What does pointer-events: all mean?

all is one of the values meant for SVG elements. The shape responds anywhere over its fill or its stroke, even if the fill is none or the shape is hidden with visibility. On ordinary HTML elements, use auto and none.

Why is my pointer-events: none not working?

Check which element really catches the click. The rule is often on a parent while a child or a sibling with a higher z-index still covers the target. Right click the spot and choose Inspect to see which element the browser picks.

Keep reading