Simplicity is an engineering decision.
Four principles for better CSS, JavaScript, PHP and design. Their real value lies in knowing what to simplify, what to share and what to leave alone.
The basic principle
Good websites can be sophisticated without being unnecessarily complicated to maintain or use. These four principles help us question extra machinery, keep implementations understandable, avoid building features nobody needs yet and give shared rules a clear home. They are guides to judgment rather than instructions to make everything shorter. The aim is to remove complexity that serves no purpose while keeping the safeguards, structure and visual character that make the website work well.
A stylesheet with fewer lines can be harder to change. A reusable component can make every screen depend on every other screen. A clever abstraction can hide a very ordinary decision.
Simplicity is not a line count, an aesthetic or an excuse to skip difficult work. It is a property of a system that people can understand, operate and change without discovering a new set of hidden conditions at every turn.
Four familiar principles help us discuss that property: Occam’s razor, KISS, YAGNI and DRY. They address different questions. Treating them as interchangeable instructions to “make everything smaller” misses most of their value.
Four principles. Four different questions.
- Occam’s razor: do not introduce complexity without necessity. What evidence requires this additional explanation or mechanism?
- KISS: keep the implementation as simple as practicable. Can another person follow how it works and what happens when it fails?
- YAGNI: do not build things until you actually need them. Is this capability required, or are we financing an imagined future?
- DRY: do not maintain the same logic in multiple places. Which representation owns this rule, and what must change when the rule changes?
Those are working interpretations for development, not four mathematical laws. Occam’s razor originates in reasoning about explanatory economy; applying it to architecture is a useful analogy, not proof that the least elaborate implementation is correct. A simpler explanation still has to account for the evidence.
Occam’s razor: investigate before adding machinery
A navigation item has the wrong colour. One response is another selector. Then a more specific selector. Then !important. Another response is to inspect the cascade and discover that an obsolete state rule still matches the item.
The second approach removes an unnecessary cause. The first accumulates instructions that future developers must mentally execute in the right order. The problem is not the existence of specificity or !important; both have defined behaviour and legitimate uses. It is adding a mechanism before understanding the mechanism already present.
In PHP, a slow page might trigger proposals for a new cache service, a search cluster or a replacement plugin. A trace may instead reveal the same remote lookup being performed inside a loop. Resolve and reuse that request’s lookup result where its context permits. Re-measure before introducing another operational dependency.
The opposite conclusion is also possible. If profiling shows a fundamentally unsuitable query shape, a purpose-built indexed table may be justified. Occam’s razor does not require pretending that the original storage model can satisfy every workload. The mechanism earns its place by explaining and addressing an observed constraint.
For debugging, write down the observation, a plausible explanation and a test that could disprove it. “Caching” is not a diagnosis until you can identify the layer, key and stale response. Prefer explanations that fit the evidence with fewer unsupported assumptions; discard them when the evidence disagrees.
KISS: optimise for understanding, including failure
A three-line expression that mixes fetching, permission checks, formatting and fallback behaviour may be shorter than four named steps. It is usually more difficult to inspect. The relevant complexity is how much a reader must hold in their head, including the conditions hidden inside helper functions.
For a simple disclosure, native HTML can express the interaction directly:
<details class="technical-note">
<summary>What happens if the request is retried?</summary>
<p>The existing operation is resolved before any new effect is attempted.</p>
</details>This is useful for independent expandable notes. It is not automatically a substitute for a tab interface or every accordion requirement. Check keyboard behaviour, focus visibility, styling and the target browsers. The details element’s documented behaviour gives you a concrete starting contract instead of an empty div and a list of behaviours to recreate.
If custom JavaScript is necessary, define its state explicitly. A request-driven interface might have idle, loading, success and failure states. One state value can be clearer than several booleans that permit contradictory combinations such as “loading and already failed”. Define what a second click means, what cancels obsolete work and which response is allowed to update the screen.
Aborting a browser request does not establish that the server performed no work. For state-changing operations, a simple interface still needs a correct server contract. Idempotency is an example of complexity that makes the user experience simpler precisely because it handles a difficult failure mode.
YAGNI: postpone speculative capability, preserve clear boundaries
A client needs one reporting export. The implementation grows a generic report designer, a plugin system, a scheduling engine and an adapter for a database nobody uses. Each extension point has to be understood and maintained before there is evidence that it solves the right future problem.
Martin Fowler’s discussion of YAGNI distinguishes speculative capabilities from work that keeps software malleable. Avoiding an imagined feature does not require abandoning refactoring, automated verification or a useful separation of responsibilities.
For the export, write a clear query, a transformation and an output boundary. Keep the actual business rule out of the HTTP transport code. You can replace the output adapter later without designing an entire export marketplace today.
YAGNI is especially useful in ACF. A heading, introduction, theme choice and a few purposeful layout options can support a coherent editorial experience. Twenty controls for arbitrary margins, transforms, colours and per-device exceptions may create an accidental design tool that the editor must learn before publishing a paragraph.
Expose decisions editors really need to make. Introduce a new variation when an actual content requirement establishes its shape. Accessibility, authorisation and recovery for known failure modes are current requirements; they are not speculative luxuries to defer under the banner of YAGNI.
DRY: share a rule, not merely a resemblance
Two values can happen to equal twelve without representing the same decision. Combining a card gap and a booking cancellation window into one variable would be absurd, but subtler versions of the same mistake happen whenever visually similar components are forced to share an abstraction.
The authors of The Pragmatic Programmer explain DRY in terms of knowledge and intent. A useful test is whether two pieces must change together for the same reason. If they do, duplicated ownership is a maintenance hazard. If their reasons for change differ, sharing them may introduce coupling instead.
Consider a membership policy used by a template, a REST endpoint and a background job. Copying three versions of the eligibility rule invites disagreement. Give the rule one home and pass explicitly resolved facts into it:
function enjin_membership_allows_access(
string $status,
?int $expires_at,
int $now
): bool {
return $status === 'active'
&& ($expires_at === null || $expires_at > $now);
}This example defines a deliberately small policy: only active memberships qualify; a null expiry means no time limit; access ends at the expiry instant. The surrounding application must validate and resolve these facts. The function does not identify the caller, check resource ownership or replace a WordPress capability check.
Passing the current time makes the boundary testable without manipulating the system clock. Each entry point can ask the same question while retaining its own responsibility for authenticating the request and loading authoritative data.
JavaScript can present immediate feedback using public policy information, but browser validation never replaces server enforcement. If both sides must express a rule, use an appropriate shared contract or generated representation and test their agreement. Do not ship sensitive policy facts to the browser merely to eliminate a little duplication.
CSS: a small vocabulary with explicit exceptions
A design token expresses a decision. A component expresses a relationship. A variant expresses a meaningful difference. Keeping these levels distinct is often more useful than trying to eliminate every repeated declaration.
:root {
--accent: #ed492b;
--space-card: clamp(1.25rem, 3vw, 2.5rem);
}
.project-card {
padding: var(--space-card);
border: 1px solid currentColor;
}
.project-card__title {
color: var(--accent);
}
.project-card--featured {
--space-card: clamp(2rem, 4vw, 4rem);
}The featured card changes one explicit input to the existing component. It does not need a selector that describes its entire DOM ancestry. The example assumes these token names belong to the component contract; an actual design system should choose their scope deliberately.
The cascade is part of that contract. Specificity is only one stage of cascade resolution, so moving rules into layers or changing their order can matter more than adding another class. When third-party CSS is involved, establish a predictable boundary rather than escalating selector weight indefinitely.
One shared heading scale can provide consistency while a hero has an intentional display treatment. DRY does not require every heading to look identical, and KISS does not prohibit art direction. Keep exceptions named, limited and connected to a purpose.
JavaScript and PHP: reduce the distance between intent and effect
JavaScript becomes easier to reason about when a component owns its state and listeners, initialisation is safe to repeat, and teardown removes what the component registered. A small local controller may be sufficient. A framework can be justified when the interaction model, team and application warrant it; importing one does not settle that decision in either direction.
In PHP, separate a decision from the work required to obtain its inputs. A pure policy can be tested without bootstrapping a payment gateway. An adapter can map a plugin’s records into that policy’s input without teaching the policy about every plugin hook.
Do not create an interface for every function by habit. Introduce a boundary when it isolates a real source of change, side effect or testing difficulty. Conversely, do not collapse a transaction, permission decision and notification workflow into a template to claim the implementation has fewer files.
The same judgment applies to dependencies. Reimplementing a mature, complex capability poorly is not simpler than using a well-understood library. Evaluate the complete maintenance obligation: upgrades, failure behaviour, security, integration and operational support.
Design: clarity can coexist with personality
A restrained implementation can support a visually rich site. A single well-designed vector motif can appear at different scales, with purposeful variations in motion, instead of requiring an unrelated effect for every section.
Occam’s razor asks whether an extra visual device helps explain the page. KISS asks whether visitors can understand the hierarchy and operate the interaction. YAGNI questions controls and sections added for hypothetical content. DRY encourages a coherent set of type, spacing, colour and interaction rules.
None of these principles says every page must look the same. A portfolio, an enquiry form and a technical article have different jobs. Share their design language while allowing their composition to serve those jobs.
An animation earns its place through orientation, feedback or character. It also needs a considered reduced-motion alternative. A beautifully minimal form that hides its errors or loses a keyboard user’s focus is not simple for the person trying to use it.
When the principles disagree, inspect the cost of change
DRY may suggest extracting two similar implementations. YAGNI may suggest waiting until their common behaviour is understood. KISS may favour a little local repetition over a shared function with six mode flags. These are not contradictions to erase with a slogan; they expose different costs.
Ask what would happen if the next change affected only one caller. If the proposed abstraction forces unrelated consumers to negotiate that change, the boundary may be wrong. If a security rule must be corrected independently in five handlers, leaving it duplicated has a much higher cost.
At review time, ask four concrete questions: what demonstrated need does this mechanism serve; can someone trace its normal and failure paths; which parts are speculative; and who owns each rule? Then verify the behaviour that matters.
The best outcome is not the smallest repository. It is a system in which necessary complexity is visible, unnecessary complexity has been removed, and the next responsible change has an understandable place to go.
Code examples illustrate the architecture and identify application-specific interfaces. Adapt them to your environment and test the failure paths before deployment. Sources are linked alongside the relevant discussion.