Articles

CSS pointer-events Explained: Click-Through and Its Traps

CSS pointer-events: none makes an element ignore mouse, touch, and pen input so clicks pass through. How it works, good uses, and why it doesn't disable.

Takina Takina · · 4 min read
Close-up of CSS code on a screen

The CSS pointer-events property controls whether an element can be the target of pointer input — mouse clicks, hovers, touches, and pen taps. Set pointer-events: none and the element becomes invisible to the pointer: clicks, hovers, and taps pass straight through to whatever is underneath it. It’s the standard way to build click-through overlays and decorative layers, and it’s also one of the most commonly misused properties in CSS, because it looks like a “disable” switch and isn’t one.

The values you’ll actually use

For HTML elements, two values matter:

  • auto — the default. The element behaves normally.
  • none — the element is never the target of pointer events. Hit testing skips it as if it weren’t there.

SVG supports a longer list — visiblePainted, fill, stroke, all, and others — that control whether the fill, the stroke, or the whole bounding area responds. Those let you make only the painted part of an icon clickable rather than its whole box. For ordinary HTML layout you’ll only use auto and none.

How it works under the hood

When you click, the browser performs hit testing: it finds the topmost element at those coordinates, taking stacking contexts and z-index into account. That element becomes the event target, and the event bubbles up from there.

pointer-events: none removes an element from hit testing. The browser keeps looking down the stack and targets the next element it finds. If the element is a child sitting inside its parent, that next element is usually the parent itself. If it’s an absolutely positioned overlay, it’s whatever lies beneath the overlay.

Two details trip people up:

  1. It’s inherited, but children can opt back in. Setting pointer-events: none on a container affects its descendants by inheritance, but any child with pointer-events: auto becomes clickable again — even though its parent isn’t.
  2. Hit testing APIs respect it. document.elementFromPoint() skips elements with pointer-events: none, which matters if you rely on that function for custom drag-and-drop logic.

Good uses for pointer-events: none

Decorative overlays

Gradient fades, vignettes, grain textures, and ::before/::after flourishes often sit on top of interactive content. Without pointer-events: none, they swallow every click meant for the buttons beneath.

.card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(transparent, rgb(0 0 0 / 0.4));
  pointer-events: none;
}

Click-through containers with clickable children

A full-screen layer that hosts toasts or a floating toolbar shouldn’t block the page. Make the layer transparent to the pointer and let its children opt back in:

.toast-layer { position: fixed; inset: 0; pointer-events: none; }
.toast-layer > .toast { pointer-events: auto; }

Cleaner event targets

Icons inside buttons can make event.target point at an <svg> or <path> rather than the button. Setting pointer-events: none on the icon keeps the target as the button, which simplifies event delegation code that inspects event.target. Using event.target.closest('button') is the more robust fix, but the CSS approach is a common belt-and-braces addition.

During drag interactions

While dragging an element, temporarily disabling pointer events on it lets the browser detect which drop zone is beneath the cursor instead of hitting the dragged element itself.

The big trap: it doesn’t disable anything

It’s tempting to “disable” a link or button by adding pointer-events: none. That only blocks the pointer. The element is still in the tab order, still focusable, and still activatable:

  • A keyboard user can Tab to it and press Enter or Space.
  • Screen readers still announce it as an interactive control.
  • Assistive technologies can still trigger it.

So a “disabled” checkout button styled this way remains fully usable to anyone not holding a mouse — a correctness bug and an accessibility failure in one. Use the right tool instead:

GoalUseBlocks pointerBlocks keyboardExposed as disabled
Visual click-throughpointer-events: noneYesNoNo
Disable a form controldisabled attributeYesYesYes
Disable a whole regioninert attributeYesYesHidden from AT
Signal unavailable but focusablearia-disabled="true" plus JS guardNo (needs JS)No (needs JS)Yes

For links, which have no disabled attribute, either remove the href (an <a> without href isn’t focusable or activatable) or render a non-link element.

Other side effects to know

Cursors and hover styles vanish. Because the element never receives hover, cursor: not-allowed on an element with pointer-events: none never shows, and :hover styles never apply. If you want a not-allowed cursor on a disabled button, put the cursor style on a wrapper or rely on the native disabled state.

Scrolling passes through too. Wheel and touch gestures over the element target whatever is underneath, so a scrollable panel with pointer-events: none can’t be scrolled by pointer.

Text selection is affected. Users can’t start a selection inside the element with the mouse.

It’s different from touch-action. touch-action controls which browser gestures — panning, pinch-zoom — are allowed on an element that still receives events. pointer-events decides whether the element receives events at all.

The takeaway

pointer-events: none removes an element from hit testing so clicks, taps, and hovers reach whatever sits beneath it. Use it for decorative overlays, click-through layers whose children opt back in with auto, and cleaner event targets. Never use it to disable controls: keyboard and assistive-technology users can still activate the element. Reach for the disabled attribute, inert, or a removed href when you mean “this can’t be used right now.”

Takina Takina · · 4 min read

Container Query Units: cqw, cqh, and cqi Explained

Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.

#CSS #Web Development #Frontend