ComponentsForm
Inplace
A value shown as text that turns into a field on click or Enter, and saves on Enter or when the field is left.
Import
import { Inplace } from "@booleanpress/ui/inplace"Usage
Use it where people read a value far more often than they change it: a name in a details page, a subject in a list. The value is a button; pressing it opens the field with the value selected.
import { Inplace } from "@booleanpress/ui/inplace"
export function MailerName({ name, rename }: { name: string; rename: (name: string) => Promise<void> }) {
return <Inplace label="Mailer name" value={name} onSave={rename} />
}label names the field and gives the value its hint, the provider's edit string ("Edit {label}"). Leave value out and give defaultValue for an inplace that keeps its own value; give value and update it in onValueChange (or onSave) to control it. open, defaultOpen and onOpenChange control the field the same way.
Enter, the ✓ button or leaving the field saves; Escape or the × puts the value back. onSave runs with the new value (not when it is unchanged): return a promise and a spinner shows until it settles; reject it and the field stays open with the error's message, or the provider's saveFailed. While it saves, Escape and the × wait for it. A save that settles after the inplace has left the page is ignored. saveOnBlur={false} keeps the field open when focus leaves it; showButtons={false} removes the ✓ and ×.
multiline edits in a Textarea, where Enter makes a new line and Ctrl+Enter or ⌘+Enter saves; the value then keeps its line breaks when it is shown. renderDisplay draws the value your way (a badge, an image), and renderEditor puts your own control in place of the field: spread its fieldProps on the control so it takes focus and is named.
Examples
Basic
A mailer's name: click it, edit, and press Enter or the ✓.
import { Inplace } from "@booleanpress/ui/inplace"
export default function InplaceBasic() {
return (
<div className="flex w-full max-w-xs flex-col gap-1">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Mailer name</span>
<Inplace label="Mailer name" defaultValue="Primary SMTP" placeholder="Name this mailer" className="w-full" />
</div>
)
}Textarea
multiline: Enter makes a new line, Ctrl+Enter or ⌘+Enter saves.
import { Inplace } from "@booleanpress/ui/inplace"
export default function InplaceTextarea() {
return (
<div className="flex w-full max-w-sm flex-col gap-1">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Email footer</span>
<Inplace
label="Email footer"
multiline
defaultValue="You receive this email because you have an account at example.com."
className="w-full"
/>
<p className="ps-2.5 text-xs text-muted-foreground">Ctrl+Enter or ⌘+Enter saves; Enter starts a new line.</p>
</div>
)
}Controlled
value and open held by the page; a button outside opens and closes the field.
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { Inplace } from "@booleanpress/ui/inplace"
export default function InplaceControlled() {
const [name, setName] = useState("Ana Ruiz")
const [open, setOpen] = useState(false)
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Customer</span>
<Inplace label="Customer name" value={name} onValueChange={setName} open={open} onOpenChange={setOpen} className="w-full" />
<Button variant="outline" onClick={() => setOpen(!open)}>
{open ? "Close the field" : "Edit name"}
</Button>
</div>
)
}Async save with error
onSave waits for a request; a refused subject keeps the field open with the error.
import { Inplace } from "@booleanpress/ui/inplace"
// Stands in for the request that saves the subject: a subject that mentions "free" is refused.
const saveSubject = (value: string) =>
new Promise<void>((resolve, reject) =>
setTimeout(
() => (/free/i.test(value) ? reject(new Error("Subjects with “free” are refused by the spam filter.")) : resolve()),
1200
)
)
export default function InplaceAsyncSave() {
return (
<div className="flex w-full max-w-sm flex-col gap-1">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Subject</span>
<Inplace label="Subject" defaultValue="Your October invoice" onSave={saveSubject} className="w-full" />
<p className="ps-2.5 text-xs text-muted-foreground">Try a subject with the word “free” to see the error.</p>
</div>
)
}Disabled
disabled shows the value without a way to edit it.
import { Inplace } from "@booleanpress/ui/inplace"
export default function InplaceDisabled() {
return (
<div className="flex w-full max-w-xs flex-col gap-1">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Sending domain</span>
<Inplace label="Sending domain" defaultValue="mail.example.com" disabled className="w-full" />
</div>
)
}Custom display
renderDisplay shows a status badge and renderEditor a native select.
import { Badge } from "@booleanpress/ui/badge"
import { Inplace } from "@booleanpress/ui/inplace"
import { NativeSelect, NativeSelectOption } from "@booleanpress/ui/native-select"
const STATUSES = { active: "Active", paused: "Paused", archived: "Archived" } as const
type Status = keyof typeof STATUSES
const VARIANTS = { active: "success", paused: "warning", archived: "secondary" } as const
export default function InplaceCustomDisplay() {
return (
<div className="flex w-full max-w-xs flex-col gap-1">
<span className="ps-2.5 text-xs font-medium text-muted-foreground uppercase">Status</span>
<Inplace
label="Mailer status"
defaultValue="active"
showButtons={false}
renderDisplay={(value) => <Badge variant={VARIANTS[value as Status]}>{STATUSES[value as Status]}</Badge>}
renderEditor={({ value, onValueChange, fieldProps }) => (
<NativeSelect {...fieldProps} value={value} onChange={(event) => onValueChange(event.target.value)}>
{Object.entries(STATUSES).map(([key, text]) => (
<NativeSelectOption key={key} value={key}>
{text}
</NativeSelectOption>
))}
</NativeSelect>
)}
/>
</div>
)
}Accessibility
- Semantics
- Closed, a native
buttonwhose name is the value and whose description is the hint "Edit {label}". Open, arole="group"holding the field (named bylabel) and the ✓ and × buttons. While saving, the field isaria-busyand read-only; an error isaria-invalid, linked to itsrole="alert"message byaria-describedby. - Labels
labelis required: it names the field and fills the hint. The ✓ and × are named by the provider'ssaveandcancelstrings; the spinner bysaving.- Focus
- Opening moves focus into the field and selects its text. Enter, Escape, ✓ and × return focus to the value. Leaving the field for another control saves without pulling focus back. Focus on the value is a 1px
--ringoutline 2px outside it. - Known limits
- An empty value shows the placeholder; give one, or the button has no visible text.
- Escape in the field cancels the edit only: inside a dialog, sheet or popover, the outer layer stays open.
Keyboard
| Key | Behaviour |
|---|---|
| EnterorSpace | On the value, opens the field. |
| Enter | In the field, saves and returns focus to the value. In a multiline field it makes a new line. |
| CtrlEnter | In a multiline field, saves (⌘+Enter on a Mac). |
| Escape | Puts the value back, closes the field and returns focus to the value; a dialog around it stays open. While a save runs, it waits. |
| Tab | Moves to the ✓ and × buttons; leaving the field and its buttons saves. |
API
Inplace
Renders a div and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | What the value is ("Mailer name"): the field's name, and the display's hint, "Edit {label}". | |
defaultOpen | boolean | false | Whether it starts with the field open. |
defaultValue | string | The starting value, when it controls itself. | |
disabled | boolean | false | The value cannot be edited. |
multiline | boolean | false | Edit in a Textarea; Enter then makes a new line and Ctrl+Enter or ⌘+Enter saves. |
onOpenChange | ((open: boolean) => void) | Called with true or false when the field opens or closes. | |
onSave | ((value: string) => unknown) | Called with the new value when it is saved. Return a promise to show a spinner until it settles; reject it to keep
the field open with the error's message (or the provider's saveFailed). Not called when the value is unchanged. | |
onValueChange | ((value: string) => void) | Called with the new value once it is saved. | |
open | boolean | Whether the field is open, when you control it. Pair it with onOpenChange. | |
placeholder | string | Shown, muted, when the value is empty, and in the empty field. | |
renderDisplay | ((value: string) => ReactNode) | Renders the display's content from the value: a badge, an image. | |
renderEditor | ((props: InplaceEditorProps) => ReactNode) | Renders your own editor in place of the Input or Textarea. | |
saveOnBlur | boolean | true | Leaving the field saves it. true by default; false keeps it open until Enter, ✓, Escape or ×. |
showButtons | boolean | true | Renders the ✓ and × buttons after the field. |
size | "default" | "sm" | "lg" | The size of the display and the field: 28, 35 or 42 px. Defaults to the provider's controlSize. | |
value | string | The value, when you control it. Update it in onSave or onValueChange. | |
variant | "default" | "filled" | The field's look. Defaults to the provider's fieldVariant. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="inplace" (Inplace), and data-size.
Provider strings: saveFailed, edit, save, saving, cancel (BooleanUIProvider's strings).
Theming
Closed, the value has no edge and takes the --accent fill on hover, 6px radius. Open, it is the library's Input or Textarea, with the ✓ (--success) and × (--destructive) as addons on its --control edge.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--control | border |
--destructive | text |
--destructive-ghost-active | background |
--destructive-ghost-hover | background |
--destructive-strong | text |
--field | background |
--field-filled | background |
--foreground | text |
--muted-foreground | text |
--ring | outline |
--success | text |
--success-ghost-active | background |
--success-ghost-hover | background |