Conflict Banner
Inline conflict detection and optimistic-rollback UI for concurrent edit scenarios.
Overview
ConflictBanner surfaces a concurrent-edit conflict inline within the affected record — not as a global toast. It requires an explicit user decision: Keep my version or Load latest. ESC key and backdrop click are intentionally ignored.
Pair it with the useOptimisticMutation hook, which drives the full state machine: pending → conflict → resolving → resolved-local | resolved-remote.
Conflict State
The banner appears when state="conflict". Fields in the parent record should be dimmed (opacity: var(--opacity-50)) to signal that the current values are in question.
Interactive Demo
Click Simulate conflict to trigger an optimistic mutation that returns a 409 error, then resolve it with either action. The useOptimisticMutation hook manages the full state machine.
idleDiff View
Supply diffs to enable a collapsible "See what changed" panel. It's collapsed by default so casual users get a clean default, while power users can inspect specifics.
<ConflictBanner
state="conflict"
description="Jordan Kim saved changes 30 seconds ago."
diffs={[
{ field: 'Title', yours: 'Q3 Draft', theirs: 'Q3 Brief' },
{ field: 'Due date', yours: 'July 15', theirs: 'August 1' },
]}
onKeepMine={keepMine}
onLoadLatest={loadLatest}
/>useOptimisticMutation
The useOptimisticMutation hook wraps any async mutation with a built-in conflict state machine and rollback mechanism.
import { useOptimisticMutation } from '@/components/ui/conflict-banner/conflict-banner';
const {
status, // 'idle' | 'pending' | 'conflict' | 'resolving' | 'resolved-local' | 'resolved-remote'
conflictState, // ConflictState — maps directly to ConflictBanner's state prop
currentValue, // current displayed value (optimistic or rolled-back)
mutate, // (value, asyncFn) => Promise<void> — applies optimistic, calls fn, handles errors
keepMine, // () => Promise<void> — re-submits the user's version
loadLatest, // () => Promise<void> — fetches and applies the remote version
reset, // () => void — returns to idle
} = useOptimisticMutation(initialValue, {
onOptimisticApply: (v) => { /* optional: side effect when optimistic is applied */ },
onRollback: (prev) => { /* optional: side effect when rollback occurs */ },
onKeepMine: async (v) => { /* submit user's data to server */ },
onLoadLatest: async () => { /* fetch latest from server; return the value */ return remoteValue; },
});Rollback guarantees
- Atomic rollback: When
mutatethrows,currentValueis reset topreviousValueimmediately before enteringconflictstate — no partial states. - Keep my version: Calls
onKeepMine(pendingValue)and entersresolved-local. The API receives an explicit client-intent signal, not a silent clobber. - Load latest: Calls
onLoadLatest(), applies the returned value, and entersresolved-remote.
Installation
npx visor add conflict-bannerThis copies two files into your project:
components/ui/conflict-banner/conflict-banner.tsx— the component + hookcomponents/ui/conflict-banner/conflict-banner.module.css— the styles
Usage
import {
ConflictBanner,
useOptimisticMutation,
} from '@/components/ui/conflict-banner/conflict-banner';
function RecordForm() {
const { conflictState, currentValue, mutate, keepMine, loadLatest } =
useOptimisticMutation(record, {
onKeepMine: async (v) => await api.put('/records/1', v),
onLoadLatest: async () => (await api.get('/records/1')).data,
});
return (
<div>
<ConflictBanner
state={conflictState}
description="Another user saved this record while you were editing."
onKeepMine={keepMine}
onLoadLatest={loadLatest}
/>
<div style={{ opacity: conflictState === 'conflict' ? 0.5 : 1 }}>
{/* your fields here */}
</div>
</div>
);
}API Reference
ConflictBannerProps
No props data available for “conflict-banner”.
The component also accepts all standard <div> HTML attributes.
Accessibility
- Renders with
role="alert"andaria-live="assertive"— conflict is announced immediately. - ESC key does NOT dismiss the banner; explicit user action is required.
- Diff toggle uses
aria-expandedandaria-controlsfor screen reader compatibility. - Buttons are disabled (not hidden) during the resolving state so focus is preserved.
CoherenceCheck
Check group with pass/warn/fail rows for displaying coherence audit results — group header, state icon circle, title, description with inline code support, and ghost Fix action.
ErrorPlacard
Inline failed-load placard with a destructive icon chip, title, body, and optional recovery actions.