Skip to content

Interactive by exception

A static article ships no JavaScript for its body. Every live example changes that, so each one should be an argument the prose cannot make on its own.

Sofia Lindqvist

Sofia Lindqvist

Staff Frontend Engineer

8 min read
Share

An article is the cheapest thing a framework can render. Headings, paragraphs, a few highlighted code blocks — all of it is produced at build time and served as bytes that no JavaScript needs to touch. The reader gets text on first paint and nothing runs.

Then someone adds a live example, and the article is no longer that. It is now an application with an article in it.

That is often the right trade. This post is full of live examples, and each one is here because the alternative was worse. But it is a trade, and the useful discipline is to make it deliberately: interactive by exception, not by default.

What an interactive block actually costs

Three separate things, and they are worth separating because only one of them is the one people talk about.

Transfer. The component's own code, plus anything it imports. This is the cost everyone measures and the smallest of the three on a site that already ships a framework.

Hydration. The block has to be reconciled on the client before it responds. Until then it is a picture of a control — visible, focusable if you built it from real elements, and inert. A reader who taps during that window gets nothing and has no way to know why.

Comprehension. The one nobody budgets for. A control implies there is something to find by using it. If there isn't — if flipping the toggle just re-renders the same idea in a different colour — the reader has spent attention and got nothing back, and the next control in the article starts with less credit.

The test: could the prose have done it?

Here is the rule I keep coming back to. An interactive block earns its place when the thing it shows is combinatorial — when the point is not one state but the relationship between many, and enumerating them in prose would take a table nobody reads.

A chart of one dataset is not combinatorial. It is a picture, and a picture is an image file.

Chart as a component

Vue
<script setup>
import { Line } from 'vue-chartjs'
import { Chart, registerables } from 'chart.js'
Chart.register(...registerables)
</script>

<template>
  <Line :data="data" :options="options" />
</template>

Chart as an image

Markdown
![Bundle size by release](/charts/bundle-by-release.svg)
Same figure, one data set, no interaction. The second version has no hydration cost and no loading state, because there is nothing to load.

The first version is a reasonable thing to reach for and completely disproportionate to what is being shown. It pulls a charting library into the article's bundle so that a static figure can be drawn on the client instead of at build time, and it introduces a frame in which the reader sees empty space where the chart will be.

The second version renders a figure. That is all the page needed.

Now compare that to a case where the states are the point.

When it earns it

A component's prop surface is combinatorial by definition. The argument of a design system is that the variants are a closed set — that variant has exactly these values, size exactly these, and anything outside the table is an override rather than a variant. A screenshot is one cell of that table. Prose describing all of them is a paragraph the reader skims.

Letting them walk the table is the argument:

Save changes

variant

size

loading

<UiButton variant="primary" size="sm">Save changes</UiButton>

That block is the real UiButton from this codebase, not a reproduction of it, and the snippet underneath is what you would paste into a .vue file to get the state you are looking at. Both of those matter more than the interaction: an example that has drifted from the component it documents is worse than no example, because it is confidently wrong.

Interactive does not mean client-only

The most common mistake is not shipping too much JavaScript. It is making the content depend on the JavaScript.

If the hidden pane of a comparison is rendered with v-if, that content does not exist in the served HTML. It is not in the page's find-in-page. It is not in the search index. Someone who reads with JavaScript disabled, or on the connection where the bundle times out, gets an article with a hole in it.

Content behind a condition

Vue
<section v-if="mode === 'after'">
  <slot name="after" />
</section>

Content behind a style

Vue
<section v-show="mode !== 'before'">
  <slot name="after" />
</section>

One character of difference in intent, and a large difference in what the page is. v-show leaves both panes in the DOM and toggles their visibility, so the document is complete before hydration and the control is an affordance layered over it rather than a prerequisite for reading it.

The same principle decides what a live example may hold. A quiz answer sitting in the served HTML is readable by anyone who opens devtools — and that is fine, because a self-check in an article is not an assessment. Building an endpoint to hide a value from a reader who is only cheating themselves is ceremony, and it converts a static page into one that cannot be cached.

Quick check

An interactive block renders its content with `v-if` and hydrates on the client. What is the first thing that breaks?

Accessibility is not a later pass

An interactive block in an article is held to exactly the same standard as one in a product, and it usually gets less scrutiny because it looks like content. Two things go wrong most often.

The first is a control that is a div. It has a click handler and a hover state and no role, no focusability and no keyboard behaviour, and it is invisible to anyone not using a mouse. Building it from a button costs nothing and gets all three for free.

The second is state communicated only by colour. A tinted row is a result to someone who can see the tint and nothing at all to someone who cannot. If the block's entire purpose is to tell the reader whether they were right, that verdict has to be in text and in the accessibility tree, not in a hue.

The list

Before adding a live example, this is what I check:

Before an interactive block goes in

0/6 done

None of that is exotic. It is the same bar the rest of the interface is held to — which is the actual point. An article that ships components is shipping software, and the moment we stop calling it content is the moment it starts getting reviewed properly.

  • #Nuxt
  • #Performance
  • #Content
Share

Related reading

Engineering6 min read

Your component API is a contract, so write it down

Variant tables, prop naming and the boolean that should have been an enum. Notes from maintaining component libraries other people have to use.

Sofia LindqvistSofia Lindqvist

One useful email a month

Design system patterns, front-end techniques and notes from the learn platform. No promotions, no digest of other people’s links.

Unsubscribe anytime. We never share your address.