Loft
2026-08-04Study 02Interactive study

Show people their own edit before the server agrees.

Work items and owners; reassigning an owner applies instantly and settles or returns when the server answers.
ItemOwnerStateActions
Berth 12 surveyAdaHeld
Hull lines fairingGraceHeld
Batten inventoryKatherineHeld
Chalk order, Q3MargaretHeld
Floor regrid, aft bayRadiaHeld
Offsets book auditAnnieHeld

All rows held.

Why it feels right

  • The edit paints before the server answers because waiting to show someone their own action is a lie about who is in charge.
  • Pending is a state, not an animation. The row says Pending in magenta — the running color — and nothing shimmers, because shimmer decorates uncertainty instead of naming it.
  • Rejection restores the committed value and says Returned in buff. The flash happens once, on the state change, and never under reduced motion — motion only where meaning changes.
  • The reducer is three transitions and owns every rule; the component just schedules the server's answer. Pure logic first is what makes the optimistic path testable at all.

Source

export type RowState = {
  id: string;
  label: string;
  owner: string;
  committed: string;
  pending?: boolean;
  rejected?: boolean;
};

export type RowAction =
  | { type: "edit"; id: string; owner: string }
  | { type: "settle"; id: string }
  | { type: "reject"; id: string };

/* The whole study in three transitions: an edit paints instantly and
   remembers what the server last confirmed; settle promotes the paint
   to truth; reject restores the confirmed value and says so. */
export function optimisticReducer(
  rows: RowState[],
  action: RowAction,
): RowState[] {
  return rows.map((row) => {
    if (row.id !== action.id) return row;
    switch (action.type) {
      case "edit":
        return {
          ...row,
          owner: action.owner,
          pending: true,
          rejected: undefined,
        };
      case "settle":
        return {
          ...row,
          committed: row.owner,
          pending: undefined,
          rejected: undefined,
        };
      case "reject":
        return {
          ...row,
          owner: row.committed,
          pending: undefined,
          rejected: true,
        };
    }
  });
}
Access
A real table: column headers, row headers, a caption, and a polite live region narrating pending and returned states. Buttons disable while their row is in flight.
Reduced motion
The returned flash is suppressed; state changes land instantly.
Budget
0 ms