Writing code for HUMANS

By Lea Verou (@LeaVerou), MIT, W3C

…but isn’t code for machines?

Code is…

So its User Experience (UX) matters. Its usability matters. Its UI matters.

Many of you might hear these terms, UX, UIs, usability, and think they don’t apply to what you do. That these are for designers to worry about. However, in essence, these terms are not specific to the graphical user interfaces (GUIs) we've learned to associate them with. Let’s take a look at some definitions.

UX? Usability? UI?
What are we now, designers?!
🙄

User experience (UX) refers to a person's total experience using a particular product, system or service.”

Usability is the ease of use and learnability of a human-made object.”

“The User Interface (UI) is everything designed into an information device with which a human being may interact”

Handbook of Research on Web Information Systems Quality

A book is also a UI. It’s an information device with which a human being interacts.
In fact, even these definitions can be broadened. I bet you all know what I mean if I say that these boots have poor usability, and they have nothing to do with technology or information at all!

Your code is a UI

Remember the I in API stands for “Interface”

Some of you may be thinking, but code goes hand in hand with documentation. Nobody uses an API without documentation, so surely we can just explain how things work there? Yes, and no. Yes, you can rely on documentation *a little more* than with typical GUIs. Yes, people will look at *some* documentation before using your API. But think back to when you use an API for the first time. Do you exhaustively read all the docs, and learn everything there is to know about this API? No you don’t. You skim through the docs to find what you need to get started, then jump back to writing code. Don’t expect users of your API to do anything more.

But what about documentation?

Where am I coming from?

Day job: Usable Programming @ MIT

Research

Teaching

CSS Working Group Member since 2012

Elected W3C TAG member since Jan 2021 🥳

What is the W3C TAG?

What does the TAG do?

10
commandments
of good API Design

Start from use cases, not functionality

How many of you remember this? It’s one of the older DOM APIs, and has existed in browsers for at least 20 years. It allows you to check how the position of two DOM notes relates. It returns a bitmask and to interpret the result, you need to do a bitwise AND with that result. I’m sure this seemed very flexible at the time, but in practice the main use case it was used for was to check if an element contains another. And it's an insanely complicated way to do that. It can do a lot more, but developers didn’t need a lot more, they needed a way to check if A contains B!

			let isBinA = !!(a.compareDocumentPosition(b) &
				Node.DOCUMENT_POSITION_CONTAINS);
		
let isBinA = a.contains(b);
The very first step in all API design is to understand the use cases and user needs you are addressing. For any API idea, sketch out example code first, before implementing anything. How does it look? Does it read well? Does it make sense to someone that hasn't read any documentation? Does it require tedious boilerplate? If the answer to any of those is negative, go back to the drawing board. Often with user-centered design, the API might end up being simpler than what you originally thought you needed. This is because a lot of complexity is often only included for completeness, and not actually guided by real user needs.

Start with use cases, and example code before committing to any API shape

Make the simple easy & the complex possible

> let circle = document.querySelector("circle")
> circle.cx
< SVGAnimatedLength {baseVal: SVGLength, animVal: SVGLength}
> circle.cx.baseVal
< SVGLength {unitType: 1, value: 50, valueInSpecifiedUnits: 50, valueAsString: "50"}
> circle.cx.baseVal.value
< 50
<circle cx="50" cy="50" r="40" />

Three ways:

Shortcuts


			// Complex/rare case
			let evt = new KeyboardEvent("keyup", {
				key: "Enter", code: "Enter",
				shiftKey: true
			});
			textarea.dispatchEvent(evt);
		

			// Simple/common case
			button.click();
		

Shortcuts

Configurability


				// Common case: standard, minified JSON
				JSON.stringify(data);
			

				// Complex case: custom serialization, formatting
				JSON.stringify(data, (key, value) => {
					if (value instanceof HTMLElement) {
						return value.outerHTML;
					}

					return value;
				}, "\t");
			

Polymorphism


				// Simplest case: replace string with string
				text.replaceAll("    ", "\t");
			

				// Common case: replace regex with string
				text.replaceAll(/^\s+/mg, "\t");
			

				// Complex case: replace with function
				text.replaceAll(/-[a-z]/g, match => match.toUpperCase());
			

Polymorphism

I mentioned the ease of use - power curve in the previous slide. What does this mean? Every API call lies somewhere in these two axes. The vertical axis is how easy the API call was to make, and the horizontal axis is how much power it gave you. Now, let's look at a few examples. An API that is super easy to use but only allows you to do basic things is suboptimal. An API that gives you a lot of power, but is very complicated is also suboptimal. Ideally you want a curve where basic use cases start easy, and as they become more complex you can trade off some of that ease for power. This orange curve is not ideal either, note how fast it goes to low ease of use. Ideally you want something that drops much slower, not demanding a huge tradeoff for slightly more complexity.
Expressive Power Ease of use

“Simple things should be simple,
complex things should be possible.”
― Alan Kay

Common !== Simple


				// Common case: get safe HTML from untrusted input
				const sanitizer = new Sanitizer();
				container.replaceChildren( sanitizer.sanitize(untrusted_input)) );
			

				// Complex case: configure what’s allowed
				const sanitizer = new Sanitizer({
					allowElements: ['span', 'em', 'strong', 'b', 'i'],
					allowAttributes: {'class': ['*']}
				});
				container.replaceChildren( sanitizer.sanitize(untrusted_input)) );
			

Simple Common things should be simple,
complex things should be possible.”
― Lea’s paraphrase of Alan Kay’s maxim

Unnecessary error conditions

“The best error message is the one that never shows up”

Thomas Fuchs

> document.body.removeChild(div)
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
element.remove();
> document.documentElement.insertBefore(ref, div)
Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

Strive for a single source of truth. Inputs should be independent.

👎🏽
element.before(ref);

Preventing error conditions !== silent failure

> let ctx = canvas.getContext("2d", {alpha: false})
> let ctx2 = canvas.getContext("2d", {alpha: true});
> ctx === ctx2
< true
> ctx2.getContextAttributes().alpha
< false

Consistency

Internal consistency refers to consistency with other elements in a system

Internal consistency

External Consistency refers to consistency with other systems.

External consistency

Consistency helps users transfer knowledge across different APIs


			let ctx = canvas.getContext("2d");
			ctx.font = "bold 48px serif"; // Just like CSS’s font property
			ctx.fillStyle = "hsl(330 50% 50%)"; // Just like CSS <color> values
			ctx.fillText("Consistency", 0, 0);
		

Consistency…

Who remembers these? While this never became a standard (thank goodness!), before IE10, it was the only way to do gradients in CSS without images. Before IE9 it was the only way to do semi-transparent colors in IE without images. So millions of developers have written this. Not only is this syntax incredibly verbose, full of bizarre abbreviations (progid? DX?), and even includes a company name (!), it is also as non-idiomatic to CSS as it gets. Nowhere else does CSS use colons and periods to namespace functions, equals signs within a function for key-value pairs, or quoted strings for colors. While this is comically bad, we do get a lot of proposals for adding features to CSS or HTML with syntax that is utterly inconsistent with the rest of the language.

			filter: progid:DXImageTransform.Microsoft.gradient(
					startColorstr='#9acd32',
					endColorstr='#80000000'
				);
		

When different types of consistency are at odds, err on the side of the narrower internal consistency

i.e. API > language > Web Platform > external precedent
Here is another example where different types of consistency may be at odds. When we introduced Lab and LCH colors to CSS, originally L was a plain number from 0 to a 100. However, this was inconsistent with CSS, where lightnesses are expressed in percentages. Therefore, it was decided that we'd go with percentages, even though it was inconsistent with all usage of Lab colors everywhere else, because consistency within CSS was more important than consistency with external precedent.

			color: lab(50% 30 40);
		

Linear gradients in SVG


			<!-- SVG Gradient: -->
			<linearGradient id="g" gradientTransform="rotate(-50 0 .7)">
				<stop position="0" stop-color="yellowgreen">
				<stop position=".3333" stop-color="gold">
				<stop position=".6666" stop-color="red">
				<stop position="1" stop-color="black" stop-opacity=".5">
			</linearGradient>
		

Linear gradients in Canvas


			let x1 = canvas.width, y0 = canvas.height;
			let gradient = ctx.createLinearGradient(0, y0, x1, 0);
			gradient.addColorStop(0, 'yellowgreen');
			gradient.addColorStop(1 / 3, 'gold');
			gradient.addColorStop(2 / 3, 'red');
			gradient.addColorStop(1, 'rgb(0 0 0 / .5)');
		

			/* CSS linear gradient: */
			background: linear-gradient(to top right,
			                            yellowgreen,
			                            gold,
			                            red,
			                            rgb(0 0 0 / .5));
		
There is often a tension between API ergonomics and internal consistency, when precedent is very clearly of poor usability. In some cases it makes sense to break consistency to improve usability, but the improvement in usability should be very significant to justify this.

It may make sense to break consistency to improve usability, but the improvement must be significant

Sensible defaults

> new URL("foo.txt")
Uncaught TypeError: Failed to construct 'URL': Invalid URL
> new URL("foo.txt", location)
< URL {href: "https://projects.verou.me/jsux/foo.txt", …}

Sensible defaults allow API complexity to be progressively revealed

> CSS.registerProperty({name: "--color", syntax: "<color>"});
Uncaught TypeError: Failed to execute 'registerProperty' on 'CSS': required member inherits is undefined.
> CSS.registerProperty({name: "--color", syntax: "<color>", inherits: true});
Uncaught DOMException: Failed to execute 'registerProperty' on 'CSS': An initial value must be provided if the syntax is not '*'

Sometimes other considerations can get in the way of optimal usability

Order parameters by frequency of use

👎 history.pushState(null, "", "https://lea.verou.me"); // (actual)
👍 history.pushState("https://lea.verou.me"); // (fictional)

Order parameters by frequency of use

👎 JSON.stringify(data, null, "\t"); // (actual)
👍 JSON.stringify(data, "\t"); // (fictional)

Argument traps

element.cloneNode(true);
element.clone({deep: true})

			element.addEventListener("focus", callback, true);
		

			element.addEventListener("focus", callback, {
				capture: true
			});
		

			element.addEventListener("focus", callback, {
				capture: true,
				once: true
			});
		

			let event = document.createEvent('KeyboardEvent');
			event.initKeyEvent("keypress", true, true, null, null,
			                   true, false, true, false, 9, 0);
		

			let event = new KeyboardEvent("keypress", {
				ctrlKey: true,
				shiftKey: true,
				keyCode: 9
			});
		
oldChild.parentNode.replaceChild(newChild, oldChild)
oldChild.parentNode.replaceChild(oldChild, newChild)
oldChild.replaceWith(newChild)
// Change these numbers to see what each one is for...
ctx.ellipse(100, 100, 50, 75, .785398);

			ctx.ellipse({
				center: [100, 100],
				radius: [50, 75],
				rotation: .785398
			});
		

			ctx.ellipse({
				cx: 100,
				cy: 100,
				rx: 50,
				ry: 75,
				rotation: .785398
			});
		

“Code is written once but read many times”

Unknown

Object literal argument


			(50000).toLocaleString("en", {
				style: "currency",
				currency: "EUR",
				currencyDisplay: "name"
			}); // "50,000.00 euros"
		

Be liberal in what you accept
(Robustness Principle)

👎🏽
> new URL("https://lea.verou.me")
< URL {href: "https://lea.verou.me", …}
> new URL(new URL("https://lea.verou.me"))
< URL {href: "https://lea.verou.me", …}
> new URL(location)
< URL {href: "https://projects.verou.me/jsux/index.html", …}

Ways to be liberal (in what you accept)


			for (let img of document.querySelectorAll("img:nth-of-type(n+4)")) {
				img.setAttribute("loading", "lazy");
				img.setAttribute("importance", "low");
				img.setAttribute("decoding", "async");
			}
		

			let images = document.querySelectorAll("img:nth-of-type(n+4)");

			images.setAttribute({
				loading: "lazy",
				importance: "low",
				decoding: "async"
			});
		

Remember jQuery?


			$("img:nth-of-type(n+4)").attr({
				loading: "lazy",
				importance: "low",
				decoding: "async"
			});
		
parent.append(element1, "Some text", element2)
parent.append(...[element1, "Some text", element2])
> primes = new Set([2, 3, 5])
< Set {2, 3, 5}
> primes.add(7, 11, 13)
< Set {2, 3, 5, 7}
> new Set(42)
Uncaught TypeError: number 42 is not iterable (cannot read property Symbol(Symbol.iterator))
> new Set([42])
< Set {42}

			Object.defineProperty(obj, "property1", descriptor1);

			Object.defineProperties(obj, {
				property1: descriptor1,
				property2: descriptor2,
				...
			});
		

Speak the user’s language

element.querySelectorAll(".foo")
element.find(".foo")

Describe what it does, not how it works

element.addEventListnener("click", callback)
element.bind("click", callback)
element.on("click", callback)
element.addEvent("click", callback)
element.getBoundingClientRect().bottom
element.rect().bottom
element.rect.bottom

Use few simple
words, whole

Parting
words

Cherish the n00b within

Code with empathy