Notes on the 'on' attributes
-
This was mainly generated by ChatGPT - personally I would prefer the industry hadn't rejected them, because they could be an important pattern for making lots of things less clunky (which I'll cover in another post).
-
TL;DR
-
Inline
on*
attributes are discouraged today because:-
They break modern CSP and reopen XSS holes.
-
They tangle behaviour into markup, hurting readability and reuse.
-
They scale poorly (one handler, no delegation, global scope).
-
They lose caching and optimization benefits of external scripts.
-
Tooling and frameworks assume you aren’t using them, so you swim upstream.
-
-
Once you adopt module-based JS and a strict CSP, avoiding
on*
attributes stops feeling like a rule and starts feeling like the path of least resistance. -
When people warn you off the “on*” attributes (
onclick=""
,onchange=""
,onmouseover=""
, etc.) they’re really pointing to several overlapping issues, only one of which is raw security. Here’s the full stack of reasons, roughly in the order they show up on real projects: -
1. Security & modern Content-Security-Policy (CSP)
-
XSS amplification – Inline handlers turn any injected HTML into immediately-executable JavaScript. A stray
<img onclick="stealCookies()">
in user-supplied HTML is game over. Intellipaat -
CSP blocks them by default – The moment you ship a strict CSP (
script-src 'self'
or stronger), browsers refuse to run inline event handler code, throwing “Refused to execute inline event handler…” errors. To re-enable them you have to relax the policy with nonces, hashes, or'unsafe-inline'
, which defeats much of CSP’s XSS protection. MDN Web Docsweb.dev
-
-
2. Separation of concerns & maintainability
-
Markup vs. behaviour – Mixing JS strings inside HTML scatters logic through your templates and makes refactors painful. Modern guidance (sometimes called unobtrusive JavaScript) puts all behaviour in
.js
/modules and leaves HTML as pure structure. MDN Web Docs -
Harder to read & review – Reviewers can’t skim HTML quickly when every tag may hide executable code. Linting tools and framework styleguides therefore flag inline handlers as errors or warnings. Stack Overflow
-
No re-use / duplication – An identical
onclick="doThing()"
copied onto 50 buttons is 50 copies of the same string in the HTML, versus oneaddEventListener()
call that can delegate to all buttons.
-
-
3. Scalability & flexibility
-
One handler only – HTML allows one
onclick
. Adding another means wrapping both in a stub or rewriting—the DOM API lets you attach or remove multiple listeners freely. -
Event delegation tricks – A single listener at a parent node can watch hundreds of child elements that are added/removed dynamically. That pattern doesn’t work with inline attributes.
-
Global-scope pollution – Inside
onclick="doThing()"
,doThing
must either be in global scope or written inline. Module-scoped code (ES Modules, bundlers, React/Vue/Angular components) can’t be referenced directly.
-
-
4. Performance & caching
-
Inline code is downloaded every page view – The browser can’t cache JavaScript buried in HTML; an external or bundled script is cached and usually minified/compressed. At scale that is measurable. Stack Overflow
-
Parse cost – Each attribute is parsed as HTML and then re-parsed by the JS engine. The hit is small per attribute but adds up on very large DOMs.
-
-
5. Tooling & framework compatibility
-
Frameworks transpile their own “inline-looking” syntax – In React (
onClick={…}
) or Vue (@click="…"
) you appear to write inline, but the JSX/Template compiler converts that into standardaddEventListener
calls and keeps you in module scope. Raw HTMLonclick=""
bypasses that safety net. -
Strict TypeScript / ESLint rules – Most mainstream configs (
eslint:recommended
, Airbnb, Google) and HTMLHint mark inline handlers as errors, forcing teams onto the safer patterns.
-
-
Is “security” alone a strong enough reason?
-
Yes—because a single XSS vector that bypasses CSP undoes all the other careful escaping you did. But even if you somehow proved your markup can never be influenced by users, you’d still inherit the maintainability, scalability, and tooling problems above. In other words:
-
Security is the show-stopper; the other reasons are the daily pain.
-
-
What to do instead
-
Add listeners in JavaScript
-
jsCopyEditdocument .querySelector('#save') .addEventListener('click', handleSave);
-
-
Delegate when you have many similar elements
-
jsCopyEditdocument .querySelector('.todo-list') .addEventListener('click', e => { if (e.target.matches('button.delete')) deleteItem(e.target.dataset.id); });
-
-
Use
data-*
attributes or classes as hooks – Keep semantically-meaningful HTML and let JS readdataset
values. -
Lock in a strict CSP (
script-src 'self'; object-src 'none'; base-uri 'none'…
) and verify your site still works—if it breaks, you probably still have inline JS somewhere.
-
-