4 posts on API Design

Making simple things easy and complex things possible is not enough

14 min read Report broken page

The holy grail of good API design is making complex things possible and simple things easy. But is it enough?

One of my favorite product design principles is Alan Kay’s “Simple things should be simple, complex things should be possible”. [1]

However, in the years since, I’ve come to realize that making simple things easy and complex things possible is a good first step, but for most things, it’s not enough.

Not just about APIs

Since Alan Kay was a computer scientist, his adage is typically framed as an API design principle. However, it’s a good rule of thumb for pretty much any creative tool, any user interface designed to help people create artifacts. APIs are only an example of such an interface.

The line between creative tools and transactional processes [2] is blurry. While APIs or design and development tools are squarely in the creative tool category, what about something like Google Calendar? If you squint, it could be argued that Google Calendar is a creative tool where the artifact being created is a calendar event.

Is Google Calendar a creative tool?

Indeed, Kay’s maxim has clearly been used in its design. Everything has sensible defaults that can be tweaked, so if all we want is to add an hour-long event at a specific date and time, we can do that with a single click at the right place in the calendar. Can’t really get simpler than that. We can drag an edge to make it shorter or longer, or drag & drop to reschedule it (direct manipulation), and we can tweak most details from the popup itself, or click “More Options” and get even more control (e.g. set guest permissions). Simple things are easy, and complex things are possible.

Which things are simple?

For a long time, I used to argue that the principle should be “Common things should be easy, uncommon things should be possible”. Often, the most common use cases are not at all simple!

Let’s take the HTML <video> element as an example. Simple things are certainly easy: all we need to get a nice sleek toolbar that works well is a single attribute: controls. We just slap it on our <video> element and bam, we’re done with a single line of HTML:

<video src="videos/cat.mp4" controls></video>

Now let’s suppose use case complexity increases juuuust a little bit. Maybe I want to add buttons to jump 10 seconds back or forwards. Or a language picker for subtitles. Or key moment indicators, like YouTube. Or just to hide the volume control on a video that has no audio track. None of these are particularly niche, but the default controls are all-or-nothing: the only way to change them is to reimplement the whole toolbar from scratch, which takes hundreds of lines of HTML, CSS, and JavaScript to do well.

The user experience of HTML <video> has a usability cliff.

Simple things are easy and complex things are possible. But once use case complexity crosses a certain (low) threshold, user effort abruptly shoots up.

This is called a usability cliff, and is common when products make simple things easy and complex things possible by providing two distinct interfaces: a very high level one that caters to the most common use case, and a very low-level one that lets users do whatever but they have to reimplement everything from scratch.

For delightful user experiences, making simple things easy and complex things possible is not enough — the transition between the two should also be smooth. The user effort required to achieve incremental value should be proportional to the value gained. There should be no point where incremental value requires orders of magnitude more user effort.

You can visualize this like that:

A delightful user experience has a smooth power-to-effort curve without cliffs. The slower the rare of increase, the better.

Apply the principle recursively

One good way to avoid cliffs is to ask yourself: among the use cases I considered “complex”, which ones are most common? Then make simple things easy for them too.

This was a big reason why PrismJS, a syntax highlighting library I wrote in 2012, became so popular, reaching over 2 billion downloads on npm and being used on some pretty huge websites [3].

Simple things were easy: highlighting code on a website that used good HTML took nothing more than including a CSS file and a script tag. Because its only hook was regular HTML, and there was no Prism-specific “handshake” in the markup, it was able to work across a large variety of toolchains, even tooling where authors had little control over the HTML produced (e.g. Markdown).

Complex things were possible: it included a simple, yet extensive system of hooks that allowed plugin authors to modify its internal implementation to do whatever by basically inserting arbitrary code at certain points and modifying state.

But beyond these two extremes, the principle was applied recursively: Common complex things were also easier than uncommon complex things. For example, while adding a new language definition required more knowledge than simply using the library, a lot of effort went into reducing both the effort and the knowledge required to do so. Styling required simple CSS, styling simple, readable class names. And as a result, the ecosystem flourished with hundreds of contributed languages and themes.

This is a very common pattern for designing extensible software: a powerful low-level plugin architecture, with easier shortcuts for common extensibility points.

Malleable shortcuts

A corollary of Incremental user effort for incremental value is that if the interface provides a simple way to accomplish part of a complex use case, users should be able to take advantage of it to get a headstart for more complex use cases, rather than having to recreate the solution from scratch using a more complex interface.

At their core, all ways to smoothen this curve revolve around tweaking: Making sure that the solution to simple cases is sufficiently flexible that it takes a lot of use case complexity before users need to recreate the solution from scratch using lower-level primitives, if they need to at all.

This is the core issue with the <video> example: The way it makes simple things easy is completely inflexible. There are no extensibility points, no way to customize anything. It’s take it or leave it.

While web components are not typically the poster child of good user experiences, there is one aspect of web component APIs that allows them to provide a very smooth power-to-effort curve: slots. Slots are predefined insertion points with defaults. If I’m writing a <my-video> component, I can define its default play button like this:

<button id="play">
	<slot name="play-button-icon">▶️</slot>
</button>

And now, a component consumer can use <my-video src="cat.mp4"> and get a default play button, or slot in their own icon:

<my-video src="cat.mp4">
	<i slot="play-button-icon" class="fa-solid fa-play"></i>
</my-video>

But the best thing about slots is that they can be nested. This means that component authors can defensively wrap parts of the UI in slots, and component consumers can override just the parts they need, at the granularity they need. For example, <my-video> could also wrap the default play button itself in a slot:

<slot name="play-button">
	<button id="play">
		<slot name="play-button-icon">▶️</slot>
	</button>
</slot>

And then, component consumers can still only override the icon, or override the whole button:

<my-video src="cat.mp4">
	<button slot="play-button">
		<i slot="play-button-icon" class="fa-solid fa-play"></i>
	</button>
</my-video>

Empty slots facilitate insertion points. For example, the <my-video> component author could support inserting controls before or after the play button like so:

<slot name="play-button-before"></slot>
<slot name="play-button">
	<button id="play">
		<slot name="play-button-icon">▶️</slot>
	</button>
</slot>
<slot name="play-button-after"></slot>

And then, component consumers can use them to add additional controls:

<my-video src="cat.mp4">
  <button slot="play-button-before" class="skip-backwards"><svg>…</svg></button>
  <button slot="play-button-after" class="skip-forwards"><svg>…</svg></button>
</my-video>

Given enough extension points, users would only need to resort to building custom controls from scratch when they truly have a very complex use case that cannot be implemented as a delta over the default controls. That smoothens out the curve, which may look more like this:

A custom video component that uses slots extensively can smoothen the curve.

Let’s get back to Google Calendar for another example. Suppose we want to create a recurring event. Even within the less simple use case of creating a recurring event, there are simpler use cases (e.g. repeat every week), and more complex ones (e.g. every third week, on the third Sunday of the month, twice a week etc.).

Google Calendar has used tweakable presets to make simple things easy and complex things possible at the micro-interaction level. Simple things are easy: you just pick a preset. But these presets are not just shortcuts for common cases. They also serve as entrypoints into the more “advanced” interface that can be used to set up almost any rule — with enough user effort.

Tweakable presets smoothen the curve exactly because they contain the additional user effort to only the delta between the user’s use case, and the simpler use case the interface is optimized for. By doing that, they also become a teaching tool for the more advanced interface, that is much more effective than help text, which is typically skimmed or ignored.

Google Calendar recurring event presets Google Calendar recurring event customization dialog
Google Calendar making simple things easy and complex things possible at the micro-interaction level.

A hierarchy of abstractions

So far, both malleable abstractions we have seen revolved around extensibility and customization — making the solution to simple use cases more flexible so it can support medium complexity use cases through customization.

The version of this on steroids is defining low-level primitives as building blocks, and then composing them into high-level abstractions.

My favorite end-user facing product that does this is Coda. If you haven’t heard of Coda, imagine it as a cross between a spreadsheet, a database, and a document editor.

Coda implements its own formula language, which is a way for end users to express complex logic through formulas. Think spreadsheet formulas, but a lot better. For many things, the formula language is its lowest level primitive.

Then, to make simple things easy, Coda provides a UI for common cases, but here’s the twist: The UI is generating formulas behind the scenes. Whenever users need to go a little beyond what the UI provides, they can switch to the formula editor and tweak the generated formula, which is infinitely easier than starting from scratch.

Let’s take the filtering interface as an example, which I have written about before. At first, the filtering UI is pretty high level, designed around common use cases:

Another nice touch: “And” is not just communicating how multiple filters are combined, but is also a control that lets users edit the logic.

For the vast majority of use cases, the high-level UI is perfectly sufficient. If you don’t need additional flexibility, you may not even notice the little f button on the top right. But for those that need additional power it can be a lifesaver. That little f indicates that behind the scenes, the UI is actually generating a formula for filtering. Clicking it opens a formula editor, where you can edit the formula directly:

I suspect that even for the use cases that require that escape hatch, a small tweak to the generated formula is all that is necessary. The user may have not been able to write the formula from scratch, but tweaking is easier. As one data point, the one time I used this, it was just about using parentheses to combine AND and OR differently than the UI allowed.

Smoothening the curve is not just about minimizing user effort for a theoretical user that understands your interface perfectly (efficiency), it’s also about minimizing the effort required to get there (learnability). The fewer primitives there are, the better. Defining high-level abstractions in terms of low-level primitives is also a great way to simplify the user’s mental model and keep cognitive load at bay. It’s an antipattern when users need to build multiple different mental models for accomplishing subtly different things.

When high-level abstractions are defined as predefined configurations of the existing low-level primitives, there is only one mental model users need to build. The high level primitives explain how the low-level primitives work, and allow users to get a headstart for addressing more complex use cases via tweaking rather than recreating. And from a product design perspective, it makes it much easier to achieve smooth power-to-effort curves because you can simply define intermediate abstractions rather than having to design entirely separate solutions ad hoc.

For the Web Platform, this was basically the core point of the Extensible Web Manifesto, which those of you who have been around for a while may remember: It aimed to convince standards editors and browsers to ship low-level primitives that explain how the existing high-level abstractions worked.

Low-level doesn’t mean low implementation effort

Low-level primitives are building blocks that can be composed to solve a wider variety of user needs, whereas high-level abstractions focus on eliminating friction for a small set of user needs. Think of it that way: a freezer meal of garlic butter shrimp is a high-level abstraction, whereas butter, garlic, and raw shrimp are some of the low-level primitives that go into it.

The low-level vs high-level distinction refers to the user experience, not the underlying implementation. Low-level primitives are not necessarily easier to implement, and are often much harder. Since they can be composed in many different ways, there is a much larger surface area that needs to be designed, tested, documented, and supported. It’s much easier to build a mortgage calculator than a spreadsheet application.

As an extreme example, a programming language is one of the most low-level primitives possible: it can build anything with enough effort, and is not optimized for any particular use case. Compare the monumental effort needed to design and implement a programming language to that needed to implement e.g. a weather app, which is a high-level abstraction that is optimized for a specific use case and can be prototyped in a day.

As another extreme example, it could even be argued that an AI agent like ChatGPT is actually a low-level primitive from a UX perspective, despite the tremendous engineering effort that went into it. It is not optimized for any particular use case, but with the right prompt, it can be used to effectively replace many existing applications. The floor and ceiling model also explains what is so revolutionary about AI agents: despite having a very high ceiling, their floor is as low as it gets.

Reveal complexity progressively

Another corollary of Incremental user effort should produce incremental value is also that things that produce no value should not incur user effort. Complexity should be tucked away until it’s needed. Users should not have to deal with complexity that is not relevant to them. Enterprise software, I’m looking at you.

For example, individual user accounts should not need to set up “workspaces” separately from setting up their account, or designate representatives for different business functions (legal, accounting, etc.). This is complexity that belongs to complex use cases leaking out to simple ones. Any concepts exposed through a UI should add user-facing value. If a concept does not add user-facing value, it should not be exposed to users.

And for APIs, this emphasizes the importance of sensible defaults, so that users don’t need to make a ton of micro-decisions that may be entirely irrelevant to them.

When a shorter curve is the right call

Every design principle is a rule of thumb, not a hard and fast law. Sometimes, there are good reasons not to make the curve extend across the entire spectrum.

When not motivated by user needs

Some products are framed exactly around only one end of the spectrum. While they could do better and extend their curve a little bit, their entire value proposition is around one end of the spectrum, so it doesn’t make a lot of sense to invest resources in improving the other end.

Professional tools are an example where focusing around complex things being possible may be acceptable, such as airplane cockpits, or Photoshop. Tools that require a high level of domain expertise can typically afford to require some training, as said training often happens at the same time as acquiring the domain expertise. For example, a pilot learns how an airplane cockpit works while also learning how to fly.

For many of these tools, use cases are so variable that making simple things significantly easier would turn them into a different product. For example, Photoshop is a professional-grade graphics editor, that can be used for a large variety of graphics-related tasks. Focusing around a specific subset of use cases, say photo manipulation, doesn’t give us a better Photoshop UI, it gives us Lightroom. Is there a way to combine the two into a single product so that users don’t need to understand when to use which tool, without making both actively worse? Perhaps, but it’s not at all obvious.

On the other hand, something like Instagram’s photo editor makes it trivial to perform simple photo manipulations that look good with very little user effort and no domain expertise (low floor), but is quite limited in its capabilities; there are many things it simply cannot do (low ceiling). While there is a lot of room for improvement, making significantly more complex things possible is largely out of scope as beyond a certain point it would require domain expertise that Instagram’s target audience lacks.

Security & privacy

Sometimes, decomposing a high-level solution into low-level primitives can introduce security & privacy issues that a more tightly coupled high-level solution can avoid.

When I was in the TAG, at some point we reviewed a proposal for a low-level API which would allow websites to read the list of fonts installed on the user’s system. This raised huge red flags about user privacy and fingerprinting. However, upon closer inspection, it turned out that nearly use cases were highly concentrated, and were all variations of the same scenario: letting end-users select a font from their system to apply it to a given artifact (e.g. a document, a graphic etc). A high-level font picker form control where the browser takes care of displaying the list of fonts and only communicates the selected font back to the application would both address privacy concerns and make the API easier to use.

Performance

Sometimes, design decisions are driven by performance considerations, rather than usability principles. For example, CSS selectors got :focus-within to match elements that contained a focused element long before :has() was added, which allows targeting ancestors in a much more generic way. There was no question that :has() would have been a better solution, but it was considered impossible to implement performantly at the time :focus-within was designed. And even today, browsers apply certain optimizations to :focus-within that make it perform better than :has().

Other times, sensible defaults are not possible because the common case is also the slowest. This is the reason why inherits is mandatory when registering CSS properties: the default that would be best for users (true) is also the slowest, but making false the default would mean that registered properties behave differently from native properties by default. Instead, it was decided to not have a default, and make the descriptor mandatory.

Which comes first, convenience or capability?

Alan Kay’s maxim only deals with what to do, not when to do it. There is no discussion around prioritization. But in the real world, the when is just as important as the what.

Sure, let’s make simple things easy and complex things possible. But which solution do you ship first? Which one do you design first?

Stay tuned for Part 2, which will cover exactly this!


  1. Kay himself replied on Quora and provided background on this quote. Don’t you just love the internet? ↩︎

  2. a distinction I first read about in Joe McLean’s brilliant post on overfitting ↩︎

  3. The other one being that it was the only one at the time that made syntax highlighting actually look good ↩︎


Make complex things possible first

7 min read Report broken page

The holy grail of good API design is making complex things possible and simple things easy. But which one do you start from?

Which comes first, convenience or capability?

Sometimes the stars align and you come up with a single solution that gives you a smooth, wide curve. But often, the way to achieve that smooth curve is to layer multiple solutions, some optimized for simple use cases and others for complex ones.

Ideally, these are not independent, but build on top of each other. For example, a high-level solution may be essentially a smart preset that configures multiple low-level primitives for a specific use case.

Alan Kay was a brilliant computer scientist, but he was very much a scientist, not a product manager. Therefore, his wise maxim does not deal with prioritization between the two.

Sure, both are important. But which one do you ship first? Which one do you design first? You can rarely do both at the same time. In the real world when you ship matters just as much as what you’re shipping.

Two fundamentally different layering strategies.

This is a controversial topic, and the right answer is generally It Depends™. But when it comes to the Web Platform, after 14 years of designing and reviewing features for it, I have concluded that unless there is a good reason for the opposite, starting by designing low-level primitives tends to be the safer bet.

Convenience for growth, power for retention

Prioritizing low-level primitives will make most product folks gasp. For regular products, it’s rare to enter a market where there is no existing product making things possible. Therefore, often the best strategy to achieve product-market-fit is to pick the right use cases and optimize the hell out of them. Additionally, since low-level primitives are often more work, the economics of shipping them are not always favorable.

The conventional product wisdom is right when the main goal is growth. It’s a common misconception among engineers that to facilitate user acquisition, you need to build something more powerful or higher quality. More often than any of those, users flock to products that reduce the floor (easier to get started) and make things easier overall. We glorify hardcore engineering but most successful software innovations have been usability innovations at their core. Stripe was just a way to make online payments easy. Dropbox could (and was) seen as a high-level abstraction over existing OS primitives, but it was easier. iPhone was easier to use than the smart phones that came before it. Instagram provided an easier way to do photo manipulation that looked good without being an expert (lower floor). Slack was easier than the market leaders of the time — and of course IRC. And the list goes on. Even the Web itself was a usability innovation. All it did at the start was already possible via FTP — but the Web provided a much easier, more streamlined way to accomplish these use cases.

So, power is not what will get you to PMF — unless you are the first to provide power, which is rare. But making complex things possible is key to retention, i.e. preventing churn. Reality is messy, and the less common use cases are bound to come up eventually. If you’ve done your homework and optimized for the right use cases, it will take enough time for that to happen that some customer loyalty will have formed and switching costs will no longer be zero. But it is almost a certainty that it will happen. And if your product has no escape hatches, if there are no workarounds that make the more complex things possible, users leave. So perhaps we rephrase Alan Kay’s maxim with a product twist: users come to a product because it makes simple things easy, and stay because it makes complex things possible.

But the Web Platform is a very unique “product”: when it comes to building websites, it has no competitor. It’s not like browsers ship with a couple alternative web platforms that web developers can use instead. Web Platform technologies only compete among themselves: if CSS or HTML doesn’t do what you need, you can often do it in JS, but these types of solutions tend to come at a cost. And when something is not possible with any Web Platform technology, users are just stuck.

When it comes to building apps, the Web Platform is competing against native platforms. And indeed, when developers switch to native platforms, it’s rarely because the Web Platform made common things hard — it’s usually because it made certain things impossible. There are still native capabilities and optimizations that the Web Platform does not expose and can still only be accessed through native platforms, e.g. the ability to react to have voice commands anywhere in the OS perform actions in the app.

Low-level solutions buy you time

Pick any creative product, or any platform, and browse its user feedback forum. Unless it already has a very high ceiling, you will find it’s littered with requests for capabilities, with very few requests for convenience. This is not because friction doesn’t matter. But being stuck hurts a lot more than being inconvenienced.

Users are rarely vocal about friction. When there is a workaround, however suboptimal, users often push through and forget about it. It only bubbles up as a complaint when the hassle is both significant and frequent. Often they don’t even identify friction as a problem, because they expect things to be hard. Until they see a competitor that makes things easy, and the cycle repeats.

Shipping a low-level solution that can function as a workaround for a host of use cases, even if it’s not a primary solution for any of them buys you time. It gives users a way out. They don’t have to flock elsewhere just to get stuff done. Even if its usability is abysmal, the gap can be briefly bridged with customer support and education — for a bit. It doesn’t suffice, but it reduces urgency, and buys you more time to get the high-level solution right.

And getting it right matters a lot; the stakes are higher when it comes to designing the right high-level solution. A suboptimal low-level primitive usually translates to too much friction (Hello WebRTC! How are you doing today Web Components?), but it usually still serves its core purpose of making complex things possible. But a high-level solution that misses the mark about which use cases to optimize for is practically useless.

Low-level primitives lead to better high-level solutions

Starting low-level often produces better overall designs, both in terms of a smooth power-to-convenience curve and in terms of preventing overfitting. And it does this in two ways, with both feeding into each other.

Shipping a low-level solution first means you can now collect valuable data about how it is used, and make more informed decisions about the high-level solution. Seeing what users actually do with the low-level building blocks tests your hypotheses about what what they need and how common it is.

Out of the various web technologies I’ve designed over the years, Relative Colors are definitely in the top 3 I’m most proud of. They unlocked so many possibilities for color manipulation, most of which I never imagined when I first proposed them.

Back then, we envisioned most of their usage to be fairly simple, mainly around additions, multiplications, and replacing entire components with constants. Things like this:

--color-accent: oklch(70% 0.155 205);
--color-accent-95: oklch(from var(--color-accent) 97% c h);
--color-accent-darker: oklch(from var(--color-accent) calc(l * 0.8) c h);
--color-accent-50a: oklch(from var(--color-accent) l c h / 50%);

In practice, it turned out that real-world usage required much more complex math to derive the kinds of aesthetically pleasing colors that even come close to what a designer would create [1]. It was fortunate that Relative Colors were designed as a more general low-level primitive — an eigensolution if you will — and thus could accommodate use cases far more complex than what they were originally envisioned for.

Rather than going straight for the most high-level solution right after, a common path involves progressively shipping composable shortcuts and abstractions to make common patterns of using the low-level primitives easier. But the way these are used also give you more data, so by the time you get to the high-level solution, you have an unparalleled understanding of user needs.

Use case variability as a factor

A good high-level solution addresses a high enough chunk of user needs to justify its implementation effort, as well as the additional UI complexity of integrating it. It could be framed as an instance of the Pareto principle: 80% of user needs are concentrated on 20% of use cases — the challenge is finding the right 20%.

However, there are instances where user needs are (or appear to be) so variable that it becomes very hard to carve out a group that could reasonably be addressed by the same high-level solution. In such cases, a low-level solution is the only viable approach.

And on the other extreme, there are instances where user needs are so concentrated on very few use cases that a high-level solution can address nearly all of them, giving you the best of both worlds. The next section includes an example of this.

A big challenge here is that often use cases appear less varied at first than they later turn out to be, and by the time you realize your blind spots, it’s too late and you’ve already shipped a high-level solution that is overfit.

Who remembers node.compareDocumentPosition()? It was a lower-level function that returned a bitmask (!) telling you everything you may possibly want to know about the relationship between two nodes in the DOM. However, this is an instance where user needs were very highly concentrated around the same use case: testing whether en element contains another. This API made complex things possible that nobody wanted and simple things were very convoluted:

if (!!(el1.compareDocumentPosition(el2) & Node.DOCUMENT_POSITION_CONTAINS)) {
  // el1 contains el2
}

This was later recognized and a much simpler el1.contains(el2) function was added.

When decomposition introduces issues

I said that starting low-level tends to be a safer bet, unless there is a good reason not to. One such reason is when exposing lower-level primitives would involve negative security, privacy, or performance implications that a more tightly coupled high-level solution can avoid.

When I was in the TAG, at some point we reviewed a proposal for a low-level API which would allow websites to read the list of fonts installed on the user’s system. This raised huge red flags about user privacy and fingerprinting. However, upon closer inspection, it turned out that nearly use cases were highly concentrated, and were all variations of the same scenario: A web app needing to let users apply a font to a given artifact (e.g. a document, a graphic etc). A high-level font picker form control where the browser takes care of displaying the list of fonts and only communicates the selected font back to the application both addressed privacy concerns and made the API easier to use.

Power can crowdsource convenience

Not all creative tools have extensible architectures, but for those that do, shipping low-level building blocks lets power users join forces in making common things easy. E.g. if your product supports a plugin architecture, ensuring that this is sufficiently powerful means that users can also make common things easy, by authoring plugins. This benefit is not just restricted to users: it also lets you test out different ideas for high-level solutions through plugins and test the waters, without having to commit to supporting them long term and with a much lower bar than shipping them as part of the core product.

The Web Platform is the poster child for this. Indeed, this was exactly the central point of the Extensible Web Manifesto, which those of you who have been around for a while may remember: ship low-level primitives first, and then web developers can make common things easy through libraries and frameworks.

Unfortunately, as often happens, the nuance was lost in translation and the EWM ended up becoming an excuse to only work on low-level capabilities. Absolutes are easier to deal with, so humans frequently try to skew nuanced guidance towards extremes. Indeed, I would not be surprised if people try to do the same with this essay, and reply “but high-level solutions are important too!”, entirely missing the point that this is about prioritization, not picking sides.


  1. this deserves a whole other post, which is on my to-do list ↩︎


Forget “show, don’t tell”. Engage, don’t show!

4 min read Report broken page

A few days ago, I gave a very well received talk about API design at dotJS titled “API Design is UI Design” [1]. One of the points I made was that good UIs (and thus, good APIs) have a smooth UI complexity to Use case complexity curve. This means that incremental user effort results in incremental value; at no point going just a little bit further requires a disproportionately big chunk of upfront work [2].

Observing my daughter’s second ever piano lesson today made me realize how this principle extends to education and most other kinds of knowledge transfer (writing, presentations, etc.). Her (generally wonderful) teacher spent 40 minutes teaching her notation, longer and shorter notes, practicing drawing clefs, etc. Despite his playful demeanor and her general interest in the subject, she was clearly distracted by the end of it.

It’s easy to dismiss this as a 5 year old’s short attention span, but I could tell what was going on: she did not understand why these were useful, nor how they connect to her end goal, which is to play music. To her, notation was just an assortment of arbitrary symbols and lines, some of which she got to draw. Note lengths were just isolated sounds with no connection to actual music. Once I connected note lengths to songs she has sung with me and suggested they try something more hands on, her focus returned instantly.

I mentioned to her teacher that kids that age struggle to learn theory for that long without practicing it. He agreed, and said that many kids are motivated to get through the theory because they’ve heard their teacher play nice music and want to get there too. The thing is… sure, that’s motivating. But as far as motivations go, it’s pretty weak.

Humans are animals, and animals don’t play the long game, or they would die. We are programmed to optimize for quick, easy dopamine hits. The farther into the future the reward, the more discipline it takes to stay motivated and put effort towards it. This applies to all humans, but even more to kids and ADHD folks [3]. That’s why it’s so hard for teenagers to study so they can improve their career opportunities and why you struggle to eat well and exercise so you can be healthy and fit.

So how does this apply to knowledge transfer? It highlights how essential it is for students to a) understand why what they are learning is useful and b) put it in practice ASAP. You can’t retain information that is not connected to an obvious purpose [4] — your brain will treat it as noise and discard it.

The thing is, the more expert you are on a topic, the harder these are to do when conveying knowledge to others. I get it. I’ve done it too. First, the purpose of concepts feels obvious to you, so it’s easy to forget to articulate it. You overestimate the student’s interest in the minutiae of your field of expertise. Worse yet, so many concepts feel essential that you are convinced nothing is possible without learning them (or even if it is, it’s just not The Right Way™). Looking back on some of my earlier CSS lectures, I’ve definitely been guilty of this.

As educators, it’s very tempting to say “they can’t possibly practice before understanding X, Y, Z, they must learn it properly”. Except …they won’t. At best they will skim over it until it’s time to practice, which is when the actual learning happens. At worst, they will give up. You will get much better retention if you frequently get them to see the value of their incremental imperfect knowledge than by expecting a big upfront attention investment before they can reap the rewards.

There is another reason to avoid long chunks of upfront theory: humans are goal oriented. When we have a goal, we are far more motivated to absorb information that helps us towards that goal. The value of the new information is clear, we are practicing it immediately, and it is already connected to other things we know.

This means that explaining things in context as they become relevant is infinitely better for retention and comprehension than explaining them upfront. When knowledge is a solution to a problem the student is already facing, its purpose is clear, and it has already been filtered by relevance. Furthermore, learning it provides immediate value and instant gratification: it explains what they are experiencing or helps them achieve an immediate goal.

Even if you don’t teach, this still applies to you. I would go as far as to say it applies to every kind of knowledge transfer: teaching, writing documentation, giving talks, even just explaining a tricky concept to your colleague over lunch break. Literally any activity that involves interfacing with other humans benefits from empathy and understanding of human nature and its limitations.

To sum up:

  1. Always explain why something is useful. Yes, even when it’s obvious to you.
  2. Minimize the amount of knowledge you convey before the next opportunity to practice it. For non-interactive forms of knowledge transfer (e.g. a book), this may mean showing an example, whereas for interactive ones it could mean giving the student a small exercise or task. Even in non-interactive forms, you can ask questions — the receiver will still pause and think what they would answer even if you are not there to hear it.
  3. Prefer explaining in context rather than explaining upfront.

“Show, don’t tell”? Nah. More like “Engage, don’t show”.

(In the interest of time, I’m posting this without citations to avoid going down the rabbit hole of trying to find the best source for each claim, especially since I believe they’re pretty uncontroversial in the psychology / cognitive science literature. That said, I’d love to add references if you have good ones!)


  1. The video is now available on YouTube: API Design is UI Design ↩︎

  2. When it does, this is called a usability cliff. ↩︎

  3. I often say that optimizing UX for people with ADHD actually creates delightful experiences even for those with neurotypical attention spans. Just because you could focus your attention on something you don’t find interesting doesn’t mean you enjoy it. Yet another case of accessibility helping everyone! ↩︎

  4. I mean, you can memorize anything if you try hard enough, but by optimizing teaching we can keep rote memorization down to the bare minimum. ↩︎


Mass function overloading: why and how?

4 min read 0 comments Report broken page

One of the things I’ve been doing for the past few months (on and off—more off than on TBH) is rewriting Bliss to use ESM 1. Since Bliss v1 was not using a modular architecture at all, this introduced some interesting challenges.

Continue reading