By Lea Verou (@LeaVerou)

What does JS have to do with UX?

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

But what about documentation?

Make the simple easy & the complex possible

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

> 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" />

			// Complex/rare case
			let evt = new KeyboardEvent("keyup", {
				key: "Enter", code: "Enter",
				shiftKey: true

			// Simple/common case


Optional arguments

				// Common case: standard, minified JSON

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

					return value;
				}, "\t");

Function arguments

				// Common case: replace with string
				text.replaceAll(/\s+/g, " ");

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


Expressive Power Ease of use

Unnecessary error conditions

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

Thomas Fuchs

> let div = document.createElement("div")
> 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.parentNode.insertBefore(newChild, element);
> let div = document.createElement("div")
> document.documentElement.insertBefore(document.body, 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.

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", …}
> new Set(42)
Uncaught TypeError: number 42 is not iterable (cannot read property Symbol(Symbol.iterator))
> new Set([42])
< Set {42}

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", …}
> 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 '*'

Default Values

			function callback(
				timestamp = Date.now(),
				future = timestamp + 3000
			) {
				console.log(timestamp, future);

Default Values
+ destructuring

			function callback({
				timestamp = Date.now(),
				future = timestamp + 3000
			} = {}) {
				console.log(timestamp, future);

Order parameters by importance

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

Argument traps

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)
// Change these numbers to see what each one is for...
ctx.ellipse(100, 100, 50, 75, .785398);

				center: [100, 100],
				radius: [50, 75],
				rotation: .785398

				cx: 100,
				cy: 100,
				rx: 50,
				ry: 75,
				rotation: .785398

“Code is written once but read many times”


Object literal argument

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

Support bulk operations

			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)");

				loading: "lazy",
				importance: "low",
				decoding: "async"

Remember jQuery?

				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}

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

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

Speak the user’s language


Describe what it does, not how it works

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

Use few simple
words, whole


Cherish the n00b within

Code with empathy