← Back
[W] Writing

A save bar has more than four states

A save bar looks like one of the simplest components there is. It says you have unsaved changes, with a button.

I built one for HextaUI, the Save Bar block, that morphs between its states like the Dynamic Island. Here's a working version, rebuilt without React for this page. Edit the name.

Try the failure. Try leaving with unsaved changes. ⌘S works when you're in the field. Then try editing while it's saving.

The states

Everything comes from the value and what's happening to it.

  • Hidden
  • Dirty
  • Saving
  • Saved or Error
The happy path. The interesting decisions are all in what can interrupt it.

Saved holds for 1800ms. It needs to be long enough to read, since this is the confirmation you were waiting for. But it shouldn't still be there when you've moved on.

Failure keeps your draft. The worst thing a save can do is lose what you typed because the network failed. The field doesn't change at all. The message explains what happened, and Try again replaces Save.

Editing clears the error. If you change the value, the old failure is no longer true, so it goes away. The real hook does the same thing: when the value changes, error and saved both reset to idle.

Editing during a save is a race. The first response might come back after you've typed something new, and it shouldn't overwrite anything. So each save gets a number, and a response only counts if its number is still the latest.

You can't leave without a reply. When someone tries to navigate away, the bar nudges and says "Save or discard your changes first." The real one also announces it to screen readers.

The morph

The bar has to change shape to fit each state, and you can't animate to a width you don't know. So HextaUI's version measures each state's content and animates a CSS variable to that size.

The timings, straight from the block:

Width and height614ms, on a custom spring curve
New content240ms fade, after a 60ms wait
Each item after that35ms later than the last
Nudge520ms, ignored if one ran in the last 500ms
The shell moves first and slowly. The content arrives quickly on top. If they moved together, it'd look like one blob stretching.

The demo above uses a simplified curve. The real block uses linear() easing functions that are different for expanding, collapsing and exiting. A bar that collapses too bouncily looks silly, so they can't all use the same curve.

And with reduced motion on, the size just changes, without the morph.

What I'd check in yours

If you've got a save bar, or a save anything, test these.

  1. Make the save fail. Is your draft still there?
  2. Edit while it's saving. What wins?
  3. Try to leave. Does it tell you, or do you lose your work?
  4. Change the value back to what it was. Does it still say unsaved?
  5. Use only the keyboard, with a screen reader on. Do you hear the result?

Number 4 is my favourite, because it costs almost nothing and it feels like the app is paying attention.

Next