JavaScript
Last updated:
Practical JavaScript standards for progressive enhancement, readable modules, and less code on the critical path.
01 — Foundation
Why JavaScript discipline matters
Most frontend pain comes from unnecessary JavaScript — not from JavaScript itself.
JavaScript should improve the experience, not be the only reason the experience works. Too much code, too many dependencies, and too much custom behaviour that reimplements what the browser already handles creates bugs, accessibility gaps, and systems nobody wants to maintain.
These standards describe practical JavaScript for teams shipping real products: progressive enhancement, readable code, sensible modules, and less weight — not clever one-liners or framework theatre.
See code formatting and style for semicolons, quotes, and example layout.
02 — Principles
Core principles
Defaults that keep scripts small, clear, and optional.
- enhance what already works — do not replace the platform for sport
- native browser behaviour before custom code
- readability over cleverness
- smaller bundles and fewer dependencies
- accessibility and performance considered in every script
- code another developer can change safely in six months
- early return when prerequisites are missing — fail fast instead of nesting the happy path
03 — Enhancement
Progressive enhancement first
The clue is in the name: improve what already works.
Forms should submit before JavaScript adds validation polish. Navigation should work before transitions exist. Content should make sense before styling arrives. If everything breaks when a script fails to load, the architecture is wrong.
Start with HTML and CSS. Use JavaScript to make the experience better — not to justify basic functionality.
04 — Platform
Native behaviour first
Before you write a script, ask whether the browser already solves it.
Often the answer is yes. Replacing native behaviour usually creates more bugs, more accessibility work, and more meetings — usually all three.
Reach for the platform first
- form validation and submission
- buttons, links, and
details/summaryfor accordions and FAQs — not for wrapping the primary site navigation - dialog, popover, and focusable controls (where supported)
- navigation and in-page targets
- tables and semantic structure
05 — Clarity
Readability beats cleverness
Production code is not a cryptic crossword.
Write for the next developer, not for conference slides. If someone cannot understand the code quickly, rewrite it — use names, steps, and structure that explain intent.
After a group of const or let declarations, leave a blank line before the next statement — see JavaScript formatting.
Hard to maintain
const total = items
.map((item) => item.price ? item.price.tax?.rate?.value : null)
.filter(Boolean)
.reduce((sum, value) => sum + value, 0);Easier to follow
let total = 0;
for (const item of items) {
const rate = item.price?.tax?.rate?.value;
if (rate != null) {
total += rate;
}
}
export { total };06 — Structure
Maintainability and modules
Code must survive other humans — especially in legacy systems.
Avoid giant shared files that touch everything, mystery abstractions, and architecture based on vibes. Use smaller modules with clear boundaries, shared utilities only where they earn their place, and gradual refactors — not emotional rewrites over lunch.
Avoid
// common.js — 1,200 lines, unclear ownership,
// imported on every page, changes break unrelated featuresPrefer
// mobile-nav.js — one feature, one responsibility
import { initMobileNav } from './mobile-nav.js';
initMobileNav();Early return
When a function needs DOM nodes, configuration, or data that might not be there, check once at the top and return if anything is missing. Init functions often end up a few lines longer, but the main path stays flat and you do not call methods on null — fewer surprises when a query misses during a refactor, markup is incomplete in a preview, or init runs again after client-side navigation.
function initMobileNav() {
const header = document.querySelector('.site-header');
const menuButton = document.getElementById('nav-menu-button');
if (!header || !menuButton) return;
// wire toggle, aria-expanded, escape, focus…
}Prefer that over nesting the real work inside layered if (header) blocks. The guard belongs at the boundary; the module or class owns what happens after the checks pass.
07 — Events
Event handling
Use the smallest pattern that fits — not the most impressive one.
Prefer
addEventListenerin script modules- event delegation when many similar targets share one handler
- custom events when components need loose coupling at scale
- a clear event hub only in larger systems that justify it
Avoid
<button type="button" onclick="doThing()">
Save
</button>Inline handlers belong in a museum, next to table-based layouts. Keep behaviour in modules where it can be tested, reviewed, and removed.
Defensive checks are fine
Mobile nav and similar UI belong in a module or class that owns the click handlers, ARIA, and keyboard behaviour. The entry point only gathers elements and uses early return when a query returnsnull or the feature is already initialised. Queries miss during refactors and partial renders; init often runs again after client-side navigation, view transitions, or hot reload. Colocate the script with the template or component that outputs the markup when you can — guards still stop duplicate listeners and keep failures quiet while HTML is in flux.
function initMobileNav() {
const header = document.querySelector('.site-header');
const menuButton = document.getElementById('nav-menu-button');
const nav = document.getElementById('primary-nav');
if (!header || !menuButton || !nav) return;
// Module or class wires toggle, aria-expanded, escape, focus, etc.
}
initMobileNav();Use an id on one-off controls like the menu button (id="nav-menu-button"), then getElementById — not a long descendant selector. One toggle per page makes a unique id a good fit. Keep ids out of CSS; in JavaScript they are a quick, explicit lookup when you already know what you need.
08 — Performance
Performance-aware JavaScript
Prefer less JavaScript. Full stop.
Smaller bundles improve performance, resilience, and maintainability — usually at the same time. Every script should justify its weight, especially third-party tags added because “everyone uses them.”
Default rules
- keep bundles small; split code with dynamic import where it helps
- lazy-load non-critical features
- minimise DOM writes and duplicate listeners
- challenge every third-party script — analytics, widgets, tags
09 — Dependencies
Dependency discipline
Every package is a future responsibility.
Install dependencies because they solve a real problem, not because they are popular. Frameworks should address complexity you actually have — vanilla JavaScript (with TypeScript when it helps) is often enough. Reach for more only when the problem demands it.
Every npm package and every third-party script tag is another surface area: supply-chain risk, compromised CDNs, and code you did not write running in your users’ browsers. Treat new dependencies like hiring — justify them, review what they load, and plan how you would remove them.
You also hand off control of how that behaviour shows up on your site. You do not own its performance budget, its accessibility behaviour, or its release cadence. A widget can regress Core Web Vitals, break keyboard focus, or ship a breaking change on a Friday while your team owns the support inbox. Native or first-party code is slower to build but stays on your standards and your test plan.
Before adding a package or embed, ask whether you could ship a smaller first-party solution, load it only where needed, and document who is responsible when it fails.
10 — Quality
Testing and comments
Test behaviour that matters. Comment decisions, not obvious lines.
Useful tests cover logic, validation rules, and critical flows — not theatre that rebuilds half the DOM to prove a button exists. Coverage dashboards are not a religion. Test what would hurt users if it broke.
Comments to avoid
count++;
// increase countComments that help
// Keep menu open until Escape — matches mobile nav pattern;
// do not close on focus moves inside the panel.
menuPanel.addEventListener('focusout', handlePanelFocusOut);TypeScript reduces ambiguity about shapes and contracts — one of its quiet strengths. Use comments for trade-offs, legacy constraints, and business rules the types cannot express.
11 — Accessibility
Accessibility in JavaScript
Scripts create accessibility problems quickly if you are not deliberate.
Pay attention whenever JavaScript touches:
- focus order and focus traps
- keyboard access for custom controls
- modals, drawers, and disclosure widgets
- dynamic content updates and live regions
- loading and error states users can perceive
A keyboard trap is not a clever interaction — it is broken. Accessibility does not stop at HTML.
12 — Review
Before you approve
A short checklist for JavaScript in code review.
- is JavaScript necessary here?
- does native browser behaviour already solve this?
- is the code readable on first pass?
- is this the simplest sensible solution?
- does it introduce accessibility or focus risk?
- does it add bundle weight or dependency cost?
- will another developer understand and change this safely?
When in doubt, ask: Is JavaScript the best solution here?Not: “Can I solve this with JavaScript?” Those are different questions. One builds better systems; the other creates another unmaintainable bundle.