Product design / interface craft

Designing software that feels quiet

Quiet software isn’t empty and it isn’t silent. It knows when to speak, what deserves your attention, and when to leave you alone.

Adam Burns11 min read

On this page

10 sections

I’ve been building a lot of small interface pieces lately: filters, command menus, stacked toasts, a button you have to hold before it deletes anything. None of them is large enough to hide behind a design system. A button either feels right or it doesn’t.

Something kept showing up when I reviewed them. The version I wanted to keep was usually the one doing less.

Not less work. Less asking. Fewer things competing for the cursor. Less motion after the point had already been made. Fewer messages explaining what the interface could have made obvious. I started thinking of that quality as quietness.

Quiet is a behaviour

A quiet interface can be dense. A terminal can feel quiet. A spreadsheet full of data can feel quiet. On the other hand, a nearly empty landing page can be loud if everything pulses, follows the pointer and asks to be clicked.

So this isn’t an argument for beige, minimalism or hiding every control behind three dots. It’s about behaviour. The interface should be clear about what matters now, make the next action easy to find, and stop performing once the job is done.

Quiet software leaves enough room for the person using it to keep their train of thought.

That last part is what I care about. Most software isn’t the work. It sits around the work. The editor sits around the sentence, the dashboard around the decision, the tool around the thing someone is trying to fix. Making that wrapper louder doesn’t make the work better.

Treat attention like it costs something

Every badge, modal, toast, animation and unread dot spends a bit of attention. Products get noisy when they act as if that budget is unlimited.

Toasts are an easy example. They’re useful because they can confirm something without stopping you. They become irritating when every internal state change creates a new one.

toast-stack.tsx
1const AUTO_DISMISS_MS = 5_200;
2const MAX_TOASTS = 3;
3
4function showToast(nextToast: Toast) {
5  setToasts((current) =>
6    [...current, nextToast].slice(-MAX_TOASTS)
7  );
8
9  scheduleDismiss(nextToast.id);
10}
11
12function markAsSaved(id: number) {
13  setToasts((current) =>
14    current.map((toast) =>
15      toast.id === id
16        ? { id, kind: "success", title: "Changes saved" }
17        : toast
18    )
19  );
20}

The limit of three matters more than the entrance animation. So does changing “Saving…” into “Changes saved” in place. It was one action, so it gets one place on the screen. There’s no reason to make someone read the beginning and end as separate news.

Even that can be too much. If the saved state is already obvious in the thing someone is looking at, a success toast is just the interface congratulating itself. I try to reserve them for work that happens somewhere else, takes long enough to create doubt, or can fail without leaving a visible mark.

Let the common path be boring

“Boring” is useful praise for an interaction people repeat all day. It means the control behaves the way their hand expects before they’ve thought about it.

A segmented control should work with a mouse, but it should also behave like tabs from the keyboard. That means the selected tab is the one in the tab order, arrow keys move selection and focus, and the semantics aren’t invented from a pile of divs.

segmented-control.tsx
1<button
2  role="tab"
3  aria-selected={selected}
4  tabIndex={selected ? 0 : -1}
5  onKeyDown={(event) => {
6    if (event.key === "ArrowRight") {
7      event.preventDefault();
8      selectTab(active + 1, true);
9    }
10
11    if (event.key === "ArrowLeft") {
12      event.preventDefault();
13      selectTab(active - 1, true);
14    }
15  }}
16>
17  {label}
18</button>

Nobody is going to notice the roving tabIndex when it works. They will notice when focus lands on every tab separately, when the arrow keys do nothing, or when a screen reader calls the whole thing a group of buttons with no state.

I’ve ended up making the same choice at product scale. Note.space keeps notes as ordinary markdown instead of inventing a format. Tray Stack puts local servers one keystroke away instead of turning them into another dashboard to manage. Familiar primitives don’t make a product unoriginal. They leave room to be original where it helps.

Motion has to explain something

I like motion, which is exactly why I have to be suspicious of it. It’s very easy to make a dull interaction more entertaining in a prototype. It’s harder to remember that the person using it will see the same flourish for the hundredth time.

I keep motion when it explains where something came from, shows that a state changed, or stops a layout change feeling broken. I remove it when it only delays the next click.

toast-status.tsx
1const reduceMotion = Boolean(useReducedMotion());
2
3<motion.span
4  initial={
5    reduceMotion
6      ? { opacity: 0 }
7      : {
8          opacity: 0,
9          transform: "translateY(3px)",
10          filter: "blur(2px)",
11        }
12  }
13  animate={{
14    opacity: 1,
15    transform: "translateY(0px)",
16    filter: "blur(0px)",
17  }}
18  transition={{
19    duration: reduceMotion ? 0 : 0.18,
20    ease: [0.22, 1, 0.36, 1],
21  }}
22/>

This is the status change inside a toast. It moves three pixels, blurs by two, and takes 180 milliseconds. The movement is small because the old and new labels occupy the same place. A big slide would imply that something travelled across the screen when all that happened was “saving” becoming “saved”.

Reduced motion isn’t an afterthought in that code path. It removes the movement without removing the state change. That’s a useful test on its own: if turning animation off makes the interface impossible to understand, the animation is covering up a hierarchy problem.

Keyboard actions are another good test. A command menu used all day should appear immediately. By the time an opening animation has finished, the person who invoked it may already be typing. Making them wait so I can show the easing curve is backwards.

Friction can be quiet too

Quiet doesn’t always mean fast. Destructive actions are one place where a little resistance is kinder than speed.

For a hold-to-delete interaction, I used 1.4 seconds. Long enough to be deliberate, short enough that it doesn’t feel like a punishment. Releasing at any point cancels it and the progress snaps back.

hold-to-delete.tsx
1const HOLD_MS = 1_400;
2
3function startHolding(button: HTMLButtonElement, pointerId: number) {
4  button.setPointerCapture(pointerId);
5  setStatus("holding");
6  startedAt.current = performance.now();
7}
8
9function stopHolding(button: HTMLButtonElement, pointerId: number) {
10  if (button.hasPointerCapture(pointerId)) {
11    button.releasePointerCapture(pointerId);
12  }
13
14  cancelAnimationFrame(frame.current);
15  setStatus("idle");
16  setProgress(0);
17}

Pointer capture is the unglamorous part that makes it reliable. Once the press starts, releasing outside the button still reaches the button and cancels the action. Without it, the interaction looks polished until somebody changes their mind by moving away, which is the exact moment cancellation needs to work.

The timing is intentionally uneven. Holding is slow because a decision is being made. Cancelling is quick because the decision has already been made. Reusing the same duration in both directions would make the code tidier and the interaction worse.

A confirmation modal could do the same job, but it would take over the screen and ask a second question after the first click. The hold keeps the consequence attached to the action itself.

The words are part of the interface

“Something went wrong” is technically a message. It’s rarely useful.

“Couldn’t sync changes. Check your connection, then try again” says what failed and what can be done next. It doesn’t include an error code, apologise for the inconvenience or turn a small failure into an incident.

I’ve become much more interested in this kind of writing than clever button labels. Calm copy is specific. It doesn’t make a joke while somebody is worried about their data. It doesn’t say “Success!” when “Saved” is clearer. It also knows when no copy is needed because the state changed in front of you.

RPH Log Analyzer is basically this principle stretched across a whole product. A giant log file is accurate, but it isn’t an answer. The useful part is deciding which lines matter and explaining what to do about them in plain English. More information would be easier to produce. Less, chosen well, is more useful.

Defaults are design

A product can look calm and still create a lot of work. If the first thing it presents is a settings screen, the user has been handed the designer’s unfinished decisions.

Good defaults are a form of editing. They decide the normal path and keep the unusual paths available without giving all of them equal weight.

That thinking shaped Note.space more than its colours did. Notes are plain markdown. The editor doesn’t add streaks, reminders, analytics or an AI sidebar. Sharing is there when it’s needed, but the default state is still just a page and a cursor. The absence is part of the product.

It shows up in Thea differently. If the bot can answer from a source, it answers and links the source. If it can’t, it hands the question to a person instead of filling the gap with a confident guess. Refusing is sometimes the quietest useful thing software can do.

Most of it lives in code nobody sees

The visual layer gets the screenshots, but quietness usually survives or falls apart in edge cases.

In the toast stack, cards behind the front one are still mounted so the stack can expand smoothly. Visually hidden isn’t enough. If those cards remain interactive, keyboard focus can disappear behind the visible toast and a screen reader can announce content the sighted user can’t reach.

toast-stack.tsx
1<motion.div
2  inert={!expanded && positionBehindFront > 0}
3  aria-hidden={!expanded && positionBehindFront > 0}
4  style={{
5    pointerEvents:
6      !expanded && positionBehindFront > 0 ? "none" : "auto",
7  }}
8>
9  {toast}
10</motion.div>

inert, aria-hidden and disabled pointer events aren’t polish. They make the collapsed stack behave like the one visible notification it appears to be.

The same category includes clearing timers when a component unmounts, pausing dismissal while somebody is reading, ignoring a second touch during a drag, keeping focus inside the part of the interface that opened, and making Escape reliably mean “put this away”.

None of that should be noticeable. That is the point. A quiet interface isn’t one with fewer details. It’s one where the details don’t become the user’s problem.

Where I still get it wrong

My first pass is often louder than the final one. I’ll animate a state because I want to see whether it works, add supporting copy because the hierarchy isn’t settled, or keep a control visible because I haven’t decided on the default yet.

That’s not always bad. It’s easier to remove a motion test than imagine one. The mistake is treating the first successful version as finished.

I also have to check “quiet” doesn’t become an excuse for low contrast, tiny targets or controls that only reveal themselves on hover. Whispering isn’t helpful if nobody can hear it. Calm software still needs to be direct.

The useful review usually happens after the component works. I use it for a while, notice which part I’m waiting for, which message I no longer read, and which animation becomes the thing between me and the next action. Those are much easier to spot on the fiftieth use than the first.

The test I keep coming back to

Before I call an interface done, I try to use it at the speed somebody familiar with it would. Not the careful pace of a demo. The slightly impatient pace of a person who already knows what they came to do.

Does anything make them stop without a good reason? Does the result appear where they’re already looking? Can they recover without a modal turning a small mistake into a ceremony? If the motion is removed, is the state still obvious? Is there a label explaining something the layout should explain instead?

Then I try it without a mouse and with reduced motion. That catches a surprising amount of visual cleverness that only works under perfect conditions.

I don’t think software needs to disappear. It can have texture, opinion and moments of delight. I just want those things to support the work instead of competing with it.

The best description I have is still “quiet”. Not silent. Not empty. Just clear enough that, once the interface has helped, it knows to get out of the way.

Thanks for reading,

Adam Burns

© 2026 Adam Burns

Sunshine Coast, Australia · GMT+10