Save Bar
ProThe unsaved-changes bar of a settings form: it rises from the bottom edge once the form is dirty, sticks to the bottom of whatever scrolls, and sinks back on save or discard.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
import { SaveBar, SaveBarMessage, SaveBarActions } from "@/components/ui/save-bar"
<SaveBar>
<SaveBarMessage />
<SaveBarActions />
</SaveBar>Placement#
Put the bar last inside the column it saves. It is sticky to the bottom of the nearest scroller, so the same markup works in a panel that scrolls on its own (the demo above) and in a page that grows and lets the window scroll (the docked app shell drops its sheet overflow when the content fits). It stays pinned while its column is on screen and settles into its own slot at the end, so it never covers the last field. The slot is always there, which keeps the layout still when the bar comes and goes. An ancestor with overflow set that does not actually scroll would capture the sticky, so keep the column free of it.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Motion#
It rises 12px and fades in on duration-base with an ease-out, and sinks back faster on duration-fast with an ease-in, so saving feels like the bar is put away rather than dismissed. With reduced motion it only fades.
Surface#
The default bar is inverted: dark in every theme, the buttons inside resolve against the dark theme on their own. variant="surface" keeps the popover surface of the page theme for quieter screens. Edit a field to see it.
Message and actions#
The words are yours: count the pending edits, or name the actions after what they do. Keep the pair to a ghost and a primary, the primary last.
API reference#
SaveBar
The root: an in-flow slot, sticky to the bottom of the nearest scroller, holding the bar itself (role="region"). It stays mounted while closed, inert and hidden from assistive tech, so the exit is a real transition. className and the remaining props go to the bar; every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | - | Shows the bar. Pass the form's dirty state. |
| variant | "inverted" | "surface" | "inverted" | inverted is a dark bar in every theme, so it reads as a layer above the page; surface is the popover surface of the current theme. |
| label | string | "Unsaved changes" | The region's accessible name. Falls back to unsavedChanges from the nearest LabelsProvider. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed", on the bar (data-slot="save-bar-content"). |
SaveBarMessage
The muted status text, truncated when space runs out. Without children it says unsavedChanges from the nearest LabelsProvider. Forwards all native <span> props.
SaveBarActions
The row of buttons, kept from shrinking. Put the quiet action first as a ghost Button and the primary last. Forwards all native <div> props.
saveBarVariants
The tv recipe behind every part.