KQ-1325 · SETTINGS/FORM
Settings Form
A settings form built from typed sections that knows what you changed: a changed field grows an edge bar on snap with a bloom of accent washing into its row, and a save bar slides up on snap counting the changes, its digit rolling as fields change and come back. Discard plays every change backwards — text un-types and re-types, a switch's knob slides home, a choice's indicator slides back, a select's label rolls back down, a range's thumb glides home — newest edit first; Save waits on a promise, then confirms field by field with a check drawn on flick while the count rolls down, and fields the save refuses shake and keep their message. Enter and Ctrl/⌘+S save, Escape takes a changed text field back, every field has its Revert, and each control is native or a real widget.
2 unsaved changes · newest: email digest
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/settings-form
Props
- sections
- SettingsSection[]
- default defaultSettingsSections
- The form: sections with a title, a line of help and fields — text (with prefix and validate), textarea, switch, choice, select or range — each with its own defaultValue.
- value / defaultValue
- SettingsValues
- default —
- The saved values, by field id: what the draft is compared against. Controlled or uncontrolled; fields left out use their own defaultValue (settingsValuesOf builds the full set).
- defaultDraft
- SettingsValues
- default —
- Edits to start from over the saved values — a draft restored from an earlier visit. They count as changes: the fields glow and the bar is up from the first render.
- onValueChange
- (values: SettingsValues) => void
- default —
- Fires when a save lands, with every saved value. Fields the save refused keep their old value.
- onDraftChange
- (draft: SettingsValues, dirty: string[]) => void
- default —
- Fires on every change to the draft — edits, reverts and each field a rewind brings back — with the ids that differ from what is saved.
- onSave
- (changes: SettingsChange[], draft: SettingsValues) => void | { errors?: Record<string, string> } | Promise<…>
- default —
- Save was pressed (or Ctrl/⌘+S, or Enter in a text field). A promise holds the bar in Saving; resolve with { errors } to refuse some fields with a message, reject to keep everything changed.
- onDiscard
- (changes: SettingsChange[]) => void
- default —
- Discard was pressed, with the changes being undone, in the order they will rewind.
- glow
- number
- default 0.6
- How far and how bright a changed field's edge glows into its row, 0 to 1. 0 keeps only the edge bar.
- bar
- "dock" | "float" | "inline"
- default "dock"
- Where the save bar lives: docked to the panel's bottom edge, a floating pill above it, or in the flow at the form's end.
- rewind
- "sequence" | "together" | "instant"
- default "sequence"
- How Discard undoes: each field plays its change backwards newest edit first, all fields at once, or every value swaps back at once with only the glow fading.
- rewindDuration
- number
- default 520
- The longest one field's rewind may take, in ms. Short text rewinds faster.
- confirmStagger
- number
- default 110
- Time between fields confirming after a save, in ms.
- savedHold
- number
- default 1400
- How long "All changes saved" stays before the bar leaves, in ms.
- title
- string
- default "Settings"
- The panel's heading; also its accessible name unless label is given. Focus lands here when the bar leaves with focus inside it.
- description
- string
- default —
- A line under the heading.
- label
- string
- default —
- The panel's accessible name. Defaults to the title.
- saveLabel
- string
- default "Save"
- The Save button's text.
- discardLabel
- string
- default "Discard"
- The Discard button's text.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the saved values have arrived: a skeleton while loading, a message with Try again on error.
- onRetry
- () => void
- default —
- Try again was pressed while status is "error".
- sound
- boolean
- default false
- Clicks for Save, Discard and Revert, a softer falling click as each rewound field lands, and a pop for each field a save you started confirms.
- disabled
- boolean
- default false
- Read only: every control is disabled and no bar appears.
- className
- string
- default —
- Classes for the root. It is 560px tall and scrolls inside itself, the dock and float bars sticky to its bottom; pass a height class to change it.
| Prop | Type | Default | Description |
|---|---|---|---|
| sections | SettingsSection[] | defaultSettingsSections | The form: sections with a title, a line of help and fields — text (with prefix and validate), textarea, switch, choice, select or range — each with its own defaultValue. |
| value / defaultValue | SettingsValues | — | The saved values, by field id: what the draft is compared against. Controlled or uncontrolled; fields left out use their own defaultValue (settingsValuesOf builds the full set). |
| defaultDraft | SettingsValues | — | Edits to start from over the saved values — a draft restored from an earlier visit. They count as changes: the fields glow and the bar is up from the first render. |
| onValueChange | (values: SettingsValues) => void | — | Fires when a save lands, with every saved value. Fields the save refused keep their old value. |
| onDraftChange | (draft: SettingsValues, dirty: string[]) => void | — | Fires on every change to the draft — edits, reverts and each field a rewind brings back — with the ids that differ from what is saved. |
| onSave | (changes: SettingsChange[], draft: SettingsValues) => void | { errors?: Record<string, string> } | Promise<…> | — | Save was pressed (or Ctrl/⌘+S, or Enter in a text field). A promise holds the bar in Saving; resolve with { errors } to refuse some fields with a message, reject to keep everything changed. |
| onDiscard | (changes: SettingsChange[]) => void | — | Discard was pressed, with the changes being undone, in the order they will rewind. |
| glow | number | 0.6 | How far and how bright a changed field's edge glows into its row, 0 to 1. 0 keeps only the edge bar. |
| bar | "dock" | "float" | "inline" | "dock" | Where the save bar lives: docked to the panel's bottom edge, a floating pill above it, or in the flow at the form's end. |
| rewind | "sequence" | "together" | "instant" | "sequence" | How Discard undoes: each field plays its change backwards newest edit first, all fields at once, or every value swaps back at once with only the glow fading. |
| rewindDuration | number | 520 | The longest one field's rewind may take, in ms. Short text rewinds faster. |
| confirmStagger | number | 110 | Time between fields confirming after a save, in ms. |
| savedHold | number | 1400 | How long "All changes saved" stays before the bar leaves, in ms. |
| title | string | "Settings" | The panel's heading; also its accessible name unless label is given. Focus lands here when the bar leaves with focus inside it. |
| description | string | — | A line under the heading. |
| label | string | — | The panel's accessible name. Defaults to the title. |
| saveLabel | string | "Save" | The Save button's text. |
| discardLabel | string | "Discard" | The Discard button's text. |
| status | "ready" | "loading" | "error" | "ready" | Whether the saved values have arrived: a skeleton while loading, a message with Try again on error. |
| onRetry | () => void | — | Try again was pressed while status is "error". |
| sound | boolean | false | Clicks for Save, Discard and Revert, a softer falling click as each rewound field lands, and a pop for each field a save you started confirms. |
| disabled | boolean | false | Read only: every control is disabled and no bar appears. |
| className | string | — | Classes for the root. It is 560px tall and scrolls inside itself, the dock and float bars sticky to its bottom; pass a height class to change it. |