Share options

Links to related pages

Essays about code. Essays about connection. Essays about context. Critiques of coding practices.

The proximity principle, part 2

There is a right way and a wrong way to code. A very wrong way.

Time to read

, 2669 words, 4th grade

Often devs become overly enamored of a particular tool, library, framework. Rather than use that approach only where it makes sense, they use it everywhere.

This is the familiar problem expressed by the old maxim: “when all you have is a hammer, everything starts to look like a nail”.

On this page

  1. The wrong way
  2. Evaluating the DML option
  3. Conclusions

The wrong way

Recently a reader commented on an article from Craft Code that weʼd posted to The article was about two simple utility functions. One to convert DOM to JavaScript (or JSON). Another to convert it back again.

The commenter claimed that writing HTML and CSS is bad practice because these languages are “static”. The right way, he said, is to use JavaScript to generate all the DOM elements and their styling.

He provided a link to his Document Makeup Library to show how we should code web pages. And he gave an example of the kind of code you would use:

DML-inspired code example.
function pageLayout( ) {
      .headcss {
      ... add some new classes here ...
  let myheader = div("","class: headcss;")
  let myfooter = div("","class: footcss;")
  return {header, footer,.....}

const {myheader, myfooter} = pageLayout()
markdown(myheader, content1)
myfooter.innerHTML = content2

He also linked to an example of, we presume, “bad” code showing how to build a traffic signal.

Our apologies for being blunt, but this is a truly terrible approach to web development. We are utterly opposed to coding this way.

This approach, which is unfortunately not uncommon, breaks several important rules. It is OK to break rules now and then, but you should have a very good reason to do so — and no better alternatives.

The first rule this approach breaks is this: Use the right tool for the job. The tool for marking up content, and providing structure and meaning to it, is HTML. The tool for presentation and styling is CSS. JavaScript is the tool for adding behavior.

You donʼt hammer nails with a wrench and you donʼt remove bolts with a hammer.

The second rule that using JS for everything breaks is the principle of least power.

JavaScript is a Turing-complete programming language. Thatʼs way too much power for static styling and markup. It is akin to cutting butter with a chainsaw.

The third rule this approach breaks is the proximity principle.

Instead of grouping our code into easily-understood units, everything is mashed together. And it all looks the same. It takes significant cognitive effort to figure out what is structure, what is styling, and what is behavior.

A counter example

We decided to create a counter example. And then we realized that it deserved a full article. So here we are.

Here is our traffic signal.

Our traffic signal is an excellent example of a component. It is an application of component architecture.

We could have created a Web Component, but we prefer to work with Astro, which uses Vite as its bundler. Astro permits us to build reusable components with TypeScript and plain HTML and CSS. Nice.

We start with the structure of the component. That means HTML:

Defining the structure of the traffic light component.
<form class="traffic-signal">
  <label class="bulb">

  <label class="bulb">

  <label class="bulb">


Our traffic signal is a state machine with three states: stop, go, and caution. So the obvious choice for the three bulbs is radio buttons. They form a simple finite state machine, after all.

So we can now click on a radio button and turn on that bulb, which turns off the others. The bulbs are big round targets, so the easiest way to handle that is to wrap the input elements in label elements. Then we can hide the radio buttons and style the labels. Sweet!

And the labels give us a nice big target. Much bigger than the tiny radio buttons. Thatʼs good for accessibility.

To make our signal more accessible, weʼll add an “aria-live” region. We will update the content of this to “STOP”, “GO”, or “CAUTION” as the signal changes. A screen reader (or VoiceOver) will then announce the change of lights. Nice.

Is there anything here that is difficult to understand for anyone with a basic knowledge of HTML? We think not. Look at the above code again. Cognitive load is very low. The structure is immediately evident. There are no gratuitous elements.

HTML done right naturally groups like elements through nesting. The form encapsulates its fields. William of Ockham would be proud.

Now to style it:

The styling for the traffic signal component. Easy peasy.
  :root {
    --amber: 83% 0.2 82.1; /* #feba00 */
    --amber-tinted: 91% 0.1 90; /* #fbdf93 */
    --green: 59% 0.3 142.5; /* #009800 */
    --green-tinted: 87% 0.3 142.5; /* #2bff26 */
    --red: 58% 0.2 30.1; /* #d73625 */
    --red-tinted: 68% 0.2 28; /* #fb594d */

    --gray-dark: 51% 0 0; /* #666666 */
    --gray-darker: 39% 0 0; /* #454545 */
    --gray-darkest: 25% 0 0; /* #222222 */

    --bulb-color-caution: radial-gradient(oklch(var(--amber-tinted)), oklch(var(--amber)));
    --bulb-color-go: radial-gradient(oklch(var(--green-tinted)), oklch(var(--green)));
    --bulb-color-stop: radial-gradient(oklch(var(--red-tinted)), oklch(var(--red)));

    --bulb-color-unlit: oklch(var(--gray-darker));
    --case-cover: oklch(var(--gray-darkest));
    --faceplate-color: oklch(var(--gray-dark));
    --rim-color: oklch(var(--gray-darkest));

    --bulb-diameter: 20vh;
    --bulb-spacing: 1vh;

  .traffic-signal {
    align-items: center;
    background-color: var(--faceplate-color);
    border-radius: 0.75vh;
    border: 0.3vh solid var(--case-color);
    box-sizing: content-box;
    display: flex;
    flex-direction: column;
    gap: var(--bulb-spacing);
    height: calc(calc(var(--bulb-diameter) * 3) + calc(var(--bulb-spacing) * 6));
    justify-content: space-between;
    padding-block: calc(var(--bulb-spacing) * 2);
    width: calc(var(--bulb-diameter) + calc(var(--bulb-spacing) * 4));

  .traffic-signal .bulb {
    aspect-ratio: 1;
    background-color: var(--bulb-color-unlit);
    border-radius: 50%;
    border: 0.3vh solid var(--rim-color);
    box-sizing: border-box;
    display: flex;
    flex: 1;
    height: var(--bulb-diameter);
    max-height: var(--bulb-diameter);
    place-content: center;

  .traffic-signal .bulb input[type="radio"] {
    opacity: 0;

  .traffic-signal .bulb:has(input[value="stop"]:checked) {
    background: var(--bulb-color-stop);

  .traffic-signal .bulb:has(input[value="caution"]:checked) {
    background: var(--bulb-color-caution);

  .traffic-signal .bulb:has(input[value="go"]:checked) {
    background: var(--bulb-color-go);

  #alert {
    max-height: 0;
    opacity: 0;

There is not much to say about the CSS. You could do this many ways, and we may not have chosen the easiest for our first pass at it. But this does show our approach. One may always refactor.

Clear naming, attention to specificity, and good use of the proximity principle. Note that properties are in alphabetical order.

Look at lines #70 to #72 for example. It reads almost like English: the traffic signal bulb that has an input with the value “stop” and is checked gets the “stop” bulb color. No surprise there.

And lines #74 to #76 and #78 to #80 do the same for the caution and go lights. Easy peasy.

Note also the use of whitespace, both within and between lines. And the use of variables to name the properties to make it clearer what these properties do. We could have taken this further with variables such as --bulb-rim-thickness.

But this is enough. The CSS variables act the same as constants in JS. What might appear to be arbitrary values become named parameters. This reduces cognitive load still further.

Speaking of JavaScript, let us now energize the traffic signal. Weʼve kept the timing to very short intervals for demonstration purposes. But you can adjust the timing with the constants up top (in millseconds). Save some lives.

The simple script that runs the traffic signal component.
  globalThis.addEventListener("DOMContentLoaded", () => {
    const GO_TIME_IN_MILLISECONDS = 2000

    const form = document.querySelector("form")
    const bulbs = form?.elements.namedItem("bulb")
    const alert = document.querySelector("#alert")

    function energizeSignal() {
      switch (bulbs?.value) {
        case "go":
          alert && (alert.innerText = "CAUTION")
          bulbs.value = "caution"

          setTimeout(energizeSignal, CAUTION_TIME_IN_MILLISECONDS)

        case "caution":
          alert && (alert.innerText = "STOP")
          bulbs.value = "stop"

          setTimeout(energizeSignal, STOP_TIME_IN_MILLISECONDS)

          alert && (alert.innerText = "GO")
          bulbs.value = "go"

          setTimeout(energizeSignal, GO_TIME_IN_MILLISECONDS)


How difficult to grasp is this? Not very difficult at all, we think.

We wrap the code in a DOMContentLoaded event listener. This ensures that all the code loads before we energize the signal (line #2 to #36). We set three constant variables for the GO, CAUTION, and STOP times in milliseconds (lines #3 to #5).

Then we add a blank line. This separates the constants from the variables holding DOM elements. The proximity principle at work!

Now, we grab the DOM elements for the form itself, the bulbs, and our hidden alert box (lines #7 to #9). Another blank line separates these variable declarations from the function that runs the lights.

Inside the energizeSignal function (lines #11 to #33), a switch makes a state machine, mirroring our radio buttons. We switch on which “bulb” is “lit” (line #12).

Then, depending on the lit bulb (or no lit bulb), we update our alert box for anyone who canʼt see the “lights”. This permits us to listen to the lights changing. And we light the next bulb in the sequence GO-CAUTION-STOP-repeat. See lines #14 and #15 for example.

Finally, we use a setTimeout function to call our energizeSignal function recursively. We add the appropriate delay depending on the bulb (see our state changes on lines #13 to #19, #20 to #26, and #27 to #31).

At the bottom of our code and outside our energizeSignal function (line #35), we start the sequence by calling energizeSignal() for the first time.

This code epitomizes the use of design principles to reduce developer cognitive load. That means faster coding, fewer bugs, code that is easier to maintain and extend, less tech debt.

And we use JavaScript properly: to progressively enhance the code. With JavaScript disabled, you can still use the light, you just have to do it manually. After all, behavior is the province of JS.

Evaluating the DML option

Unfortunately, the commenter who stimulated all this code did not provide a working example of his own traffic signal. So we went to the DML homepage hoping to make our own. There we found this claim:

What if you could ditch all the overhead that web technology brings with it today? No HTML, CSS, Ajax, jQuery and PHP to build your web apps.

Skip all the inconsistent approaches of “modern” web design and use only Javascript to get your work done.

Letʼs examine this claim.

One of the first things to note about the DML approach is that there is actually zero “benefit” with regard to the CSS. All the DML framework does is to add complexity by wrapping the CSS in a function call: CSS(`...`). So much for doing everything in JavaScript.

Didnʼt the DML site say that we could “ditch” the CSS? Whoops! Not so fast.

No gain there. But it also makes it awkward to put this CSS in a separate file. Thatʼs by design, we suspect. But it is not reusable — a key consideration with all code.

As for the HTML? The DML approach is to use JavaScript functions to generate “DOM” objects instead. And these DOM objects correspond to — wait for it — HTML elements. This is not a new idea. It has been around for decades.

If you think about it, isnʼt this exactly what React did with its createElement function? DML might be a bit slicker, but it is effectively the same species.

There are many problems with these claims. What, for example, is “inconsistent” about using HTML, CSS, and JS as intended? Nothing. But read on to see how DML does not actually “ditch” HTML either.

On the same DML site page we found this example of how DML “improves” code:

Oh, noes! This is somehow better?
<!-- DML's "HTML" example -->
  <h1>Page 1</h1>
  This is a simple list
  <br />
  <br />

<!-- The "improved" DML example -->
    h1("Page 1")
    print("This is a simple list")
    ul(["Coffee", "Tea", "Milk"])

So has the example actually done away with HTML? We think not.

The HTML above looks like something from an amateur website circa 1995. What are those br elements doing there? Spacing? Why is that line printed straight to the body? But no matter.

This is very typical of coders who think HTML is a “toy” language simply because it is not Turing complete. Why bother using it correctly? Whoʼs gonna see it, right?

We have news for you: the browser sees.

We took a look at the source for the DML homepage (in the console, of course). It reveals dozens of br elements used for spacing. Ugh. Frankly, the HTML is terrible.

But this brings us right to our point. Even if the HTML were elegant and standards compliant, this is still a bad idea.

Look at the second example, the one showing the “DML” code. You see that h1("Page 1")? What is that if not an HTML heading element?

What is ul(["Coffee","Tea","Milk"]) if not an HTML unordered list element?

You still need to know HTML! We havenʼt “ditched” it at all. But using DML encourages you not to bother to learn it well. And the result is terrible HTML code, like that above. And this leads to bad DOM code and bad CSS as well.

Why not just use HTML and CSS properly?

In short, you havenʼt made any cognitive gain. Rather, you have accrued a loss. You now have to learn HTML and CSS and JS. And then learn “DML”, too.

This is a problem with many domain-specific languages. You have to be careful to ensure that the complexity that you add is more than offset by the complexity you remove. But DML does not remove any complexity. It doubles down.

Take ul(["Coffee","Tea","Milk"]) for example. It may look as if youʼve saved some typing, but thatʼs a pretty basic example. What happens if I need to include attributes in my li elements? Or nest different lists? We bet that it gets complicated very quickly.

Worst of all, once you complicate it up it will look nothing like a list we bet.

How much simpler to let the HTML be HTML.

We will leave it to the reader to check out the examples on the DML site, if interested. Do any of these look easy to grasp? Or is there a significant cognitive effort involved in parsing them? And yet more effort to learn the DSL?

In the end we failed to find a link to the library itself. And the documentation looked like a lot of work. So we gave up on creating a DML traffic signal.

Thatʼs a shame because we would have liked a straight comparison. Maybe the commenter will read this and provide us with one that duplicates our traffic signal exactly, but in DML.


Our commenter is hardly alone in promoting these sorts of frameworks. In our experience, there is a small subset of programmers who love this kind of coding. They are the ones who complain that HTML and CSS are not “real” programming languages.

Of course they are not! They never claimed to be. HTML is a markup language and CSS is a stylesheet language. As such, they are fit for purpose.

Donʼt know if our commenter is a member of that self-selected group, but everything points to it. Like the proverbial man with a hammer, all they have is a programming language. So everything starts to look like a programming problem. Even when it is only a matter of markup or styling.

We get an especially big kick that in this instance that language is JavaScript. For many years “real” programmers complained that JS was not a “real” programming language. This despite being Turing complete! Might be some still do.

So we donʼt begrudge our commenter his library. If it works for him, have at it!

But we would never teach our students to code this way. And we would never hire anyone who did. Or permit such a framework on any project that we ran. It violates the fundamental principle of good coding: minimize cognitive footprint.

It doesnʼt make web development easier. It makes it harder.

Some devs seem to love complicating things. Quite a few, actually. Apostasy!

Our approach is very simple. Aces in their places! Use the right tool for the job.

Let the HTML do what we created it to do. Let the CSS do its thing, too. And use JavaScript in small amounts to enhance your components or when you need an algorithm.

After all, algorithms are what programming languages are for.

And if we take the proximity principle to its logical conclusion, why not put the HTML, CSS, and JS in separate files? Our commenter says that this is spaghetti code. We disagree.

Weʼve navigated the hell that is spaghetti code and lived to tell the tale. Proper use of the proximity principle is the furthest thing from spaghetti code. It is the answer to spaghetti code.

Spaghetti code is what you get when you ignore the proximity principle and throw everything into the same bowl. Thatʼs DML.

Letʼs put our CSS, HTML, and JS in separate files. Letʼs put those files in the same component folder. Then we can open them up side-by-side in our text editor. Easy peasy. Now, we have neatly encapsulated our component. No pasta in sight.

Or we can create single-file components (SFCs). That works, too.

We prefer to keep our files short. Under a hundred lines is best. Components, functions, etc. do one thing and do it well. Single responsibility, right? And everything is composable.

We will have much more to say about this and other best coding practices in future articles. And much more about the cognitive aspects of coding, as well. Stay tuned!

Links to related pages

Essays about code. Essays about connection. Essays about context. Critiques of coding practices.

Get notified form

Get notified of site updates
Button bar

Carbon emissions for this page

Cleaner than 98% of pages tested
0.018g on first visit; then on return visits 0.012g
QR Code

Scan this code to open this page on another device.