In 2006, Tim Berners-Lee and Noah Mendelsohn published a W3C TAG finding called "The Rule of Least Power." The core idea is deceptively simple: when designing a solution, choose the least powerful language or technology that is suitable for the task.
This isn't about being primitive. It's about leverage. The less powerful the tool, the more the platform can do for you — accessibility, performance, resilience, and portability come built-in. The moment you reach for JavaScript to do what HTML already does, you're taking on responsibility the browser was ready to handle for free.
After 16 years of building frontends, I've found this principle to be one of the most consistently valuable guides in my work. It's the reason I push back when teams reach for a datepicker library, a modal component, or a form validation framework — when the browser already ships all of it.
The Power Ladder
Think of frontend technologies as a ladder of increasing power — and increasing cost:
HTML → CSS → JavaScript → Framework (React, Vue) → Library → Additional framework/meta-framework.
Every rung you climb adds: bundle size, maintenance burden, abstraction leaks, potential bugs, and things to learn. The rule says: stay as low on the ladder as the problem allows. If HTML can do it, stop there. If you need styling logic, CSS. If you need interactivity, vanilla JS. Only reach for React when you need component state and reactivity. Only add a library when the built-in APIs genuinely fall short.
Most teams climb to the top of the ladder by default and never look down. They install a form library before trying a native form. They add a modal component before discovering <dialog>. They write JavaScript for things CSS has handled for years.
Form Validation: The Most Overpowered Pattern in React
This is where the rule pays off the most. I've seen countless React codebases with controlled forms, useState for every field, custom validation functions, error state management — hundreds of lines of code that replicate what the browser does natively.
HTML form validation is incredibly powerful: required, minLength, maxLength, pattern (full regex), type="email", type="url", type="number" with min/max/step. The browser validates, shows accessible error messages, prevents submission, and announces errors to screen readers. All for free.
Combine this with React's uncontrolled form pattern (useRef + FormData) and you get the best of both worlds: React's component model with the browser's native validation. Zero re-renders on input, zero validation state to manage, zero bugs from syncing state with the DOM.
// ✅ The "least power" way — HTML does the work
function ContactForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const formRef = useRef<HTMLFormElement>(null);
return (
<form
ref={formRef}
onSubmit={(e) => { e.preventDefault(); onSubmit(new FormData(formRef.current!)); }}
>
<input name="name" required minLength={2} />
<input name="email" type="email" required />
<button type="submit">Send</button>
</form>
);
}
// Zero state. Zero re-renders. The browser validates for you.
// Works without JavaScript. Accessible by default.Compare that to the typical "React way":
// ❌ The "React way" — 40 lines for a validated form
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = () => {
const errs: Record<string, string> = {};
if (!name.trim()) errs.name = 'Required';
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) errs.email = 'Invalid';
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
submit({ name, email });
};
// Plus: controlled inputs, error rendering, re-renders on every keystroke…CSS Can Do More Than You Think
Need to show a validation error message only when a field is invalid? Don't write a useState + conditional render. CSS pseudo-classes like :invalid, :valid, :placeholder-shown, and :user-invalid can handle it. Combine them with the adjacent sibling selector (+) and you have zero-JS validation feedback.
The :not(:placeholder-shown):invalid pattern is especially powerful — it only shows the error after the user has started typing, avoiding the flash of red on page load. This is a common UX requirement that teams solve with JavaScript state. CSS does it in one line.
/* Show validation errors with zero JavaScript */
input:not(:placeholder-shown):invalid {
border-color: #d32f2f;
}
/* Show the error message next to an invalid field */
input:not(:placeholder-shown):invalid + .error-hint {
display: block;
}
/* The HTML — that's it */
/* <input type="email" required placeholder="you@example.com" /> */
/* <span class="error-hint">Please enter a valid email</span> */
.error-hint {
display: none;
color: #d32f2f;
font-size: 0.85rem;
}The <dialog> Element: Kill Your Modal Library
The HTML <dialog> element gives you: modal and non-modal modes, a built-in backdrop (styleable with ::backdrop), automatic focus trapping, Esc to close, the inert attribute on background content, and a return value via <form method="dialog">.
That's everything your modal library does — except it's zero bytes, zero dependencies, accessible by default, and already in the browser. Every modern browser supports it. I've replaced 3 different modal libraries with <dialog> in production codebases.
<!-- ❌ npm install some-modal-library -->
<!-- Bundle size: +12KB gzipped, new API to learn, z-index wars -->
<!-- ✅ HTML <dialog> — built in, accessible, zero dependencies -->
<dialog id="confirm">
<h2>Are you sure?</h2>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
<script>
// Show it
document.getElementById('confirm').showModal();
// Esc to close: free. Focus trap: free. Backdrop: free.
// method="dialog" closes it and gives you the return value.
</script>requestSubmit(), pattern, and Native Date Inputs
Three more examples of the rule in action:
requestSubmit() is the DOM API most React developers don't know about. Unlike form.submit(), which bypasses validation, requestSubmit() triggers the browser's native constraint validation. If the form is invalid, the browser shows its built-in error UI. If valid, it fires the submit event. One line of code replaces a custom validation function.
The HTML pattern attribute accepts a full JavaScript regex. Need to validate a phone number format? A product code? A postal code? pattern does it declaratively, with accessible error messages. No library needed.
HTML date inputs are controversial because they look different across browsers. But that's actually a feature — on mobile, they trigger the native date picker (scroll wheel on iOS, calendar on Android), which is far superior to any JavaScript datepicker. I actively argue with UI/UX teams to design around the native input rather than replacing it. The mobile experience alone justifies it, and on desktop the minor visual differences are an acceptable trade-off for zero bundle size and perfect accessibility.
<!-- HTML pattern attribute — regex validation, no JS needed -->
<input
type="text"
name="phone"
pattern="[0-9+\s]{7,15}"
title="Phone number (digits, spaces, +)"
required
/>
<!-- requestSubmit() — trigger native validation from JS -->
<script>
// Unlike form.submit(), requestSubmit() runs validation first
document.getElementById('myForm').requestSubmit();
// If invalid → shows native error tooltips
// If valid → fires the submit event
</script>
<!-- HTML date input — no datepicker library needed -->
<input type="date" name="birthday" min="1920-01-01" max="2026-01-01" />
<!-- Native on every browser. Better UX on mobile (native date wheel). -->Web Components: When You Need Reuse Without a Framework
Sometimes you need a reusable component that works across frameworks — or without one. That's where Web Components shine. At AXA, I built a company-wide component library using Web Components because teams used different frameworks. The components worked everywhere: React apps, plain HTML pages, CMS templates.
Web Components sit perfectly on the power ladder: more powerful than HTML/CSS alone, but less powerful (and less costly) than a framework-specific component library. They use the platform's native component model — Shadow DOM, Custom Elements, slots — rather than a framework's virtual one.
The rule of least power doesn't mean "never use powerful tools." It means: don't use them when a simpler tool does the job. Web Components are the right answer when you need cross-framework reuse. React is the right answer when you need complex state management. A npm library is the right answer when the native API is genuinely insufficient. The key is reaching for them deliberately, not by default.
How to Apply This Tomorrow
Before you add a dependency or write a custom solution, walk up the ladder: Can HTML do this? (required, pattern, type, <dialog>, <details>, <datalist>). Can CSS do this? (:invalid, :has(), scroll-snap, container queries). Can a native DOM API do this? (requestSubmit, FormData, Constraint Validation API, Intersection Observer). Can vanilla JS do this without a library?
Only when you've answered "no" at every rung should you reach for the next level of power. This doesn't slow you down — it speeds you up. Less code to write, less to maintain, fewer bugs, better performance, and built-in accessibility.
The W3C told us this twenty years ago. The browser has been getting more powerful every year since. Most of what you npm install today, the platform already provides. You just have to look down before you climb up.

