Skip to content
Astera Technology

Cancel a default without stopping event propagation

Read cancellation and propagation as two separate event flags.

Download complete case notes and answer keys ↓

Inspect the fixed example

Dispatch one supplied sample event with bubbles and cancelable true. The child calls preventDefault; inspect whether the parent still runs. This synthetic event has no navigation or submission default action.

Complete body and fixed script
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="parent"><p id="child">Local event target</p></section><output id="result"></output>
<script>const trace=[];const child=document.querySelector('#child');child.addEventListener('sample',event=>{trace.push('child');event.preventDefault();});document.querySelector('#parent').addEventListener('sample',event=>trace.push('parent:'+event.defaultPrevented));
const returned=child.dispatchEvent(new Event('sample',{bubbles:true,cancelable:true}));const result={trace,returned};
document.querySelector('#result').textContent=JSON.stringify(result);
console.log(JSON.stringify(result));
</script>
Exact console output
{"trace":["child","parent:true"],"returned":false}
Inputs and native results
InspectionSupplied input or actionResult
childpreventDefault()defaultPrevented: true
parentBubbling still reaches parentparent:true
dispatchEventcancelable: truefalse

Follow the native mechanism

preventDefault sets the canceled flag for a cancelable event, so dispatchEvent returns false. It does not set the stop-propagation flag. stopPropagation blocks later ancestors but does not itself cancel. A noncancelable event cannot be canceled by preventDefault.

Compare the material alternative

Using only stopPropagation removes the parent trace, while dispatch still returns true. No real browser default action is demonstrated by this synthetic sample.

Fresh counterexample source
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="parent"><p id="child">Local event target</p></section><output id="result"></output>
<script>const trace=[];const child=document.querySelector('#child');child.addEventListener('sample',event=>{trace.push('child');event.stopPropagation();});document.querySelector('#parent').addEventListener('sample',event=>trace.push('parent:'+event.defaultPrevented));
const returned=child.dispatchEvent(new Event('sample',{bubbles:true,cancelable:true}));const result={trace,returned};
document.querySelector('#result').textContent=JSON.stringify(result);
console.log(JSON.stringify(result));
</script>
Counterexample output
{"trace":["child"],"returned":true}

Work the boundary

With cancelable false and bubbles true, what does preventDefault change?

Show the worked answer

It leaves defaultPrevented false; the parent runs and dispatch returns true. The propagation flag remains separate.

Fresh boundary source
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="parent"><p id="child">Local event target</p></section><output id="result"></output>
<script>const trace=[];const child=document.querySelector('#child');child.addEventListener('sample',event=>{event.preventDefault();trace.push('child:'+event.defaultPrevented);});document.querySelector('#parent').addEventListener('sample',event=>trace.push('parent:'+event.defaultPrevented));
const returned=child.dispatchEvent(new Event('sample',{bubbles:true,cancelable:false}));const result={trace,returned};
document.querySelector('#result').textContent=JSON.stringify(result);
console.log(JSON.stringify(result));
</script>
Boundary output
{"trace":["child:false","parent:false"],"returned":true}

Original example files