Chapter 05 — One Thing Well

Putting It Together

This chapter is a design review. No new concepts. The previous four chapters established three tenets: do one thing, work together, communicate through a shared interface. This one applies them to a real component.

The code

Here’s a nav toggle. It does the job. It ships. You’ve written something like it.

html~/tutorials/
<!-- index.html -->
<nav id="site-nav">
  <button onclick="
    var nav = document.getElementById('site-nav');
    var isOpen = nav.classList.contains('open');
    if (isOpen) {
      nav.classList.remove('open');
      this.setAttribute('aria-expanded', 'false');
      this.style.color = '#1a1f18';
    } else {
      nav.classList.add('open');
      this.setAttribute('aria-expanded', 'true');
      this.style.color = '#5c7a3e';
    }
  " aria-expanded="false" style="background: none; border: none; font-size: 1rem; cursor: pointer;">
    Menu
  </button>
  <ul style="display: none;">
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/work">Work</a></li>
  </ul>
</nav>

<style>
  #site-nav.open ul {
    display: block;
  }
</style>

Nothing here is obviously wrong. It toggles. The ARIA attribute updates. The list shows and hides. If you needed this working by end of day, you shipped it and moved on. The problems only show up when someone else touches it, or when the design changes, or when you try to use the same pattern somewhere else on the page.

Running the three rules

Does it do one thing?

No. Count what this component is responsible for: tracking open/closed state, updating the ARIA attribute, applying a visual style change to the button, and showing or hiding the list. That’s four things happening inside a single inline handler. The state lives in the DOM (the class on #site-nav), the behavior lives in the HTML attribute, and the styling lives in two places at once (inline style on the button, a <style> block for the ul).

The button doesn’t just toggle the nav. It manages everything about the toggle.

Can it work together with other programs?

Not as written. The handler is coupled to #site-nav by ID. Copy the markup to a second page, or add a second nav, and the inline handler breaks because getElementById('site-nav') will only ever find one element. There’s no exported function, no hook, no way for another script to trigger or observe the toggle state without reading the DOM directly.

If you want to add a keyboard shortcut that opens the nav, or close it when the user clicks outside, you’re writing code that reaches into the DOM to check a class and call the same inline logic twice.

Does it communicate through a shared interface?

The color values #1a1f18 and #5c7a3e are hardcoded into the inline handler. They’re also defined somewhere in your stylesheet. If the brand colors change, you update the stylesheet and forget the inline handler, or you catch it in QA if you’re lucky. There’s no shared contract. The JavaScript doesn’t know about the CSS. They both maintain their own copies of the same truth.

The refactor

The goal isn’t to make this shorter. It’s to give each piece of code one job, and to make sure the pieces talk to each other through interfaces instead of through hardcoded values and shared DOM access.

Start by separating the concerns. CSS handles all visual styling keyed off a data attribute. JavaScript manages the state transition and nothing else.

css~/tutorials/
/* nav.css */
[data-nav-state="closed"] ul {
  display: none;
}

[data-nav-state="open"] ul {
  display: block;
}

[data-nav-state="open"] .nav-toggle {
  color: var(--color-moss);
}

The CSS reads from a data attribute and from a custom property. No color values hardcoded here. The component doesn’t know what --color-moss resolves to, and it doesn’t need to. That’s the stylesheet’s job.

The markup carries state as a data attribute, not as a style.

html~/tutorials/
<!-- index.html -->
<nav data-nav-state="closed">
  <button class="nav-toggle" aria-expanded="false">Menu</button>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/work">Work</a></li>
  </ul>
</nav>

The markup carries state through data-nav-state. No inline styles. No inline handlers. The button has a class so CSS can target it without relying on position or nesting depth.

nav.js accepts any element, so it’s not coupled to #site-nav. The function doesn’t know what page it’s on.

js~/tutorials/
// nav.js: handles toggle state
export function initNav(navEl) {
  const toggle = navEl.querySelector('.nav-toggle');
  if (!toggle) return;

  toggle.addEventListener('click', () => {
    const isOpen = navEl.dataset.navState === 'open';
    navEl.dataset.navState = isOpen ? 'closed' : 'open';
    toggle.setAttribute('aria-expanded', String(!isOpen));
  });
}

main.js is the only file that needs to know both nav.js and the page structure exist. It queries for every element with the right attribute and hands each one to initNav. Initialization is separate from the toggle logic.

js~/tutorials/
// main.js: orchestration
import { initNav } from './nav.js';

document.querySelectorAll('[data-nav-state]').forEach(initNav);

nav.js does one thing: it manages the state transition for a nav element. Setting aria-expanded alongside dataset.navState is not a second responsibility: ARIA state and visual state are coupled by definition, and setting one without the other is a bug, not a separation-of-concerns violation. It doesn’t know what colors are involved. It doesn’t know the page structure outside the element it receives. It accepts any element with the right markup and wires up the toggle. If you add a second nav, main.js finds it automatically and initNav handles it the same way.

The hardcoded color values are gone. --color-moss lives in one place, in the stylesheet. JavaScript reads state through dataset. CSS reads state through attribute selectors. They communicate through the data attribute on the element, which is the shared interface, and through custom properties, which handle the design tokens.

The habit

The series has been building toward two questions. Ask them when you sit down to write a component, and again when you’re done:

What is the one thing this does? If you can’t name it in a few words without using “and,” it’s doing more than one thing.

Can this communicate through a shared interface? Meaning: does it rely on hardcoded values that exist somewhere else, or does it read from a shared contract? Does it reach into the DOM by ID, or does it accept an element as an argument and work on whatever you hand it? A yes to that second question is also what makes the pieces work together, so both questions carry all three tenets.

These aren’t rules you apply to finished code as a cleanup pass. They’re the lens you look through while you’re building. When you reach for getElementById with a specific ID, you’re making a coupling decision. When you write a color value into a JavaScript file, you’re breaking the contract. The questions surface those decisions before they harden into patterns.

That’s what the Unix philosophy gives you: a reason to stop and think about what a piece of code is responsible for, and what it needs to know. Utilities written in the 1970s are still composable today because someone answered those questions before shipping. The bar for a nav component is lower than grep. But the questions are the same.

Reference