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.
<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>{"trace":["child","parent:true"],"returned":false}| Inspection | Supplied input or action | Result |
|---|---|---|
| child | preventDefault() | defaultPrevented: true |
| parent | Bubbling still reaches parent | parent:true |
| dispatchEvent | cancelable: true | false |
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.
<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>{"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.
<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>{"trace":["child:false","parent:false"],"returned":true}