Skip to content

A web browser’s back button is more than a link to the previous page. It is the navigational equivalent of undo. Going back should restore the previous page, exactly as it was last time we were there. Conversely, the forward button is redo. These actions traverse history — they should never mutate state.

Luckily, the browser is a capable time machine by default, at least for regular web sites. It will cache content, remember form values, reset scroll bars, and all the other things required to preserve the browser history. Just make sure to keep Cache-Control: no-store out of your response headers.

However, for web apps, where we make our own little world of routes and state, it’s up to us to tackle time travel. And while an implementation as polished as the browser’s may be out of scope, a little consideration goes a long way.

An expired session is not an error when going backwards. Navigating history should never clear data or result in a spinner. It should not show any sudden 4xx or 5xx errors because of a fetch. Previous form values should be restored, even if invalid. And navigating history should always work — even when offline.

—   ¶

CSS is neither code nor markup. It’s not a query language, a configuration language, or a DSL. Instead, CSS is a style sheet language — a world of its own. Yet we haphazardly import rules from other fields. It’s far from given that our ideas about globals, inheritance, and modularity apply to the world of CSS.

The closest thing to a style sheet is not found in programming but in design. More than anything a style sheet resembles a style guide — a codified design specification. For what is a style guide but a set of design rules, invariants, and exceptions? And in both cases the ultimate goal is design consistency.

Good design is consistent. There are ground rules that apply by default, with extensions and exceptions as required. In other words, cascading inheritance is not a complexity brought by CSS, but an inherent challenge for any design language. Whether expressed in *.css or *.js, whether the cascade is implicit or explicit, inheritance is a fundamental property of good design.

This perspective informs one approach to writing CSS where we embrace cascading over isolation and inheritance over repetition. Instead of expecting consistent application of a written guide to isolated components, we use our limited pool of developer discipline for explicit reliance on global styles.

In this approach, any style that can be global should be global. Local styles should rely on styles defined further up the chain. Whenever possible, prefer unitless values and percentages to absolute measures. Prefer declarative centering to explicit offsets. Prefer intrinsics like currentColor to variables. Prefer em to rem and rem to px. Prefer extensions, and never reset.

—   ¶

It is usually more productive to talk about feature scope than about priorities. While slight shifts in priority are seldom actionable, scoping requires decisions. Scoping scales products by their number of features, and not by overall quality. Tradeoffs must be made and features must be cut — at every level of the stack.

The first step in scoping features is to get everyone on the same page. For this, words are often not enough. A shared understanding is better achieved through detailed sketches and interactive mockups. Too much code is written based on a false assumption of agreement. Make a feature obvious, then decide its scope.

The next step is to decide on which features to keep and which features to cut. Scoping is about tradeoffs. Each potential feature has an expected cost, utility, and interplay with other features. These tradeoffs should be difficult as they decide what the product will be. And iteration does not preclude planning.

When features are fleshed out and scoped in it is finally time to build. At this point there should be no fundamental questions unanswered or important thoughts unsaid. If there are split opinions it is time to disagree and commit. Any feature of uncertain scope should be pushed for re-planning in the next iteration. Plan more, code less, and always know where you’re going.

—   ¶

It has long been difficult to write test helpers in Go because you’ve had to keep track of file names and line numbers to exclude helpers from output. This has resulted in some impressive but complicated assertion libraries. Recently, Go 1.9 improved the situation greatly by introducing the Helper method:

The new Helper method, added to both testing.T and testing.B, marks the calling function as a test helper function. When the testing package prints file and line information, it shows the location of the call to a helper function instead of a line in the helper function itself. — The Go Blog

To try this out, I wrote myself a little assertion library. And it’s great! Go can finally have test helpers that tick every box: intuitive signatures, minimal additional complexity, and accurate reporting of file names and line numbers.

package example

import (


func TestExample(t *testing.T) {
    is.Equal(t, 1, 1)
    is.Nil(t, nil)
    is.Zero(t, "")
    is.Error(t, errors.New("error"))

    is.Above(t, 1, 3)
    is.Slice(t, []int{})
    is.Element(t, []int{1, 2, 3}, 3)
    is.Substring(t, "example", "ample")

See the project page for more details.

—   ¶

The ultimate goal of frontend development, as a specialized field that sits between digital design and backend development, should be to disappear — to innovate itself out of existence. And I think we’re getting close.

In time, more frontend tasks will be done by designers, though not in code. Prototypes are reaching a fidelity comparable to the finished product. It won’t be long until design tools learn to export complete components as actual code. Picture a tool between Sketch and Unity — an integrated design environment.

When design tools export trees of connected components, developers won’t have to infer intent from static mockups. We’ll no longer need the lossy translation step called a handover. What’s left is connecting endpoints to components, components to routes, and serving the resulting app to users.

While serving the content far from simple, it’s always the same: compile, optimize, minify, compress, distribute, localize, cache, serve. These are tasks to be solved once, by a generic proxy or an external service. But what about the rest of frontend — the wiring glue between routes, components, and endpoints?

These wires are just another layer of code, another step of the stack. It’s a layer as complex as any other, but not one that warrants its own field of development. Any technique unique to frontend should change to match the rest of the stack. Practices should apply across layers, and layers are better when built together.

—   ¶