build69
build69  /  Components  /  Page Structure  /  ContextualSaveBar

ContextualSaveBar

v1.0.7Updated 27 Aug 2026

Polaris-style dirty-form save bar. A portal-rendered bar docked to the top or bottom edge that slides in while a form has unsaved changes, with a Discard action (optional confirm dialog) and a Save action with loading sp

Preview

Live · 9 stories
DefaultOpen ↗
Why this is recommendedsavedirtyunsavedform+5

The default, out-of-the-box ContextualSaveBar.

Recommended for+14
  • Settings / profile edit pages
  • Record detail forms (customer, invoice, product)
  • Bulk-edit screens where changes accumulate before an explicit save
Tags+6
savedirtyunsavedformbarpolarisdiscardbeforeunloaddefault
Features+4
  • Controlled (open) or uncontrolled (defaultOpen) visibility
  • Top or bottom docking with slide-in animation (skipped under prefers-reduced-motion)
  • Portal-rendered — document.body (fixed) or a custom container element (absolute)
  • Save with loading state — controlled `saving` flag or automatic from a Promise-returning onSave
  • Save disabled while `invalid`
  • Discard with optional confirm dialog (confirmDiscard: true or custom copy)
  • Customizable message + secondary detail slot
  • Opt-in beforeunload tab-close guard while dirty
  • Opt-in Ctrl/⌘+S save shortcut
  • Polite live-region announcement + labelled role="region" for screen readers
  • useDirtyState helper with stable (key-order-independent) JSON comparison
  • i18n: labels prop overrides the built-in screen-reader strings (region aria-label, unsaved-changes announcement); visible copy already has message/saveLabel/discardLabel props — see ContextualSaveBarLabels / DEFAULT_CONTEXTUAL_SAVE_BAR_LABELS
Not recommended for−2
  • Transient feedback (use Sonner toasts)
  • Wizard/step flows with per-step commit (use Wizard)
  • Auto-saving forms with no explicit save action

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "ContextualSaveBar" })

Usage

import { ContextualSaveBar, useDirtyState } from '@/components/patterns/contextual-save-bar';

const dirty = useDirtyState(saved, form);

<ContextualSaveBar
  open={dirty}
  confirmDiscard
  onSave={async () => { await api.save(form); setSaved(form); }}
  onDiscard={() => setForm(saved)}
/>

Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
open

Controlled visibility — pass the dirty flag (e.g. from useDirtyState)

boolean
defaultOpen

Uncontrolled initial visibility; bar closes itself after save/discard

boolean
onOpenChange

Visibility change callback

(open: boolean) => void
onSave

Save handler; a returned Promise drives the spinner and closes on resolve

() => void | Promise<void>yes
onDiscard

Discard handler — reset form state here

() => voidyes
saving

Controlled saving flag (spinner + locked actions)

boolean
invalid

Disables Save (e.g. validation errors)

boolean
message

Unsaved-changes message (default "Unsaved changes")

ReactNode
detail

Secondary line under the message

ReactNode
position

Docking edge (default top)

'top' | 'bottom'
confirmDiscard

Confirm dialog before discarding; object customises copy

boolean | ContextualSaveBarDiscardConfirmOptions
confirmBeforeUnload

Register beforeunload guard while the bar is open

boolean
saveOnShortcut

Opt-in Ctrl/⌘+S triggers Save while open

boolean
container

Portal target; defaults to document.body

HTMLElement | null
labels

Override the built-in screen-reader strings — region aria-label + unsaved-changes announcement (i18n). Visible copy uses message/saveLabel/discardLabel/confirmDiscard props. Merged over DEFAULT_CONTEXTUAL_SAVE_BAR_LABELS.

Partial<ContextualSaveBarLabels>

Related