Appearance
InlineEditable
A family of composable components for inline editing. From headless primitives to ready-to-use implementations, choose the level of control you need.
Overview
The InlineEditable system provides a layered architecture for inline editing:
useInlineEdit- Core hook for state management (see hook docs)InlineEditable- Headless component with render propsEditableTrigger- Reusable trigger button with edit iconEditableActions- Reusable save/cancel buttonsInlineEditableField- Complete form field implementationEditableTableCell- Table-specific implementation (see table docs)
This architecture lets you:
- Start simple with
InlineEditableField - Customize gradually by composing primitives
- Go headless with
InlineEditablefor full control
Component Family
InlineEditable (Headless)
The core headless component. You provide the rendering logic.
When to use:
- Custom UI requirements
- Need full control over markup
- Building your own editable components
tsx
import { InlineEditable } from '@/shared/ui/components/editable/InlineEditable'
<InlineEditable
value={name}
onSave={async (value) => await api.updateName(value)}
renderDisplay={({ value, onStartEdit }) => (
<button onClick={onStartEdit}>{value}</button>
)}
renderEditor={({ value, onChange, onSave, onCancel, isSaving, inputProps }) => (
<div>
<input
value={value}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
autoFocus
/>
<button onClick={onSave} disabled={isSaving}>Save</button>
<button onClick={onCancel}>Cancel</button>
</div>
)}
/>EditableTrigger
A reusable trigger button that looks like inline content with a hover edit icon.
Features:
- Ghost button styling
- Edit icon on hover
- Keyboard accessible
- Disabled state support
tsx
import { EditableTrigger } from '@/shared/ui/components/editable/EditableTrigger'
<EditableTrigger onClick={() => startEdit()}>
<span className="font-medium">{userName}</span>
</EditableTrigger>EditableActions
Reusable save/cancel action buttons with consistent styling.
Features:
- Check (✓) and X icons
- Loading state support
- Disabled state support
- Size variants
tsx
import { EditableActions } from '@/shared/ui/components/editable/EditableActions'
<EditableActions
onSave={handleSave}
onCancel={handleCancel}
isSaving={isSaving}
size="sm"
/>InlineEditableField
A complete, ready-to-use form field implementation.
Features:
- Label support
- Custom display/input rendering
- "Edited" badge
- Save on blur option
- Error display
- Keyboard shortcuts
tsx
import { InlineEditableField } from '@/shared/ui/components/editable/InlineEditableField'
<InlineEditableField
id="user-name"
label="Name"
value={user.name}
onSave={async (value) => await updateUser({ name: value })}
placeholder="Enter name..."
edited={hasChanges}
/>Quick Start
Basic Example (InlineEditableField)
The simplest way to get started:
tsx
import { InlineEditableField } from '@/shared/ui/components/editable/InlineEditableField'
export function UserProfile({ user }) {
const handleSave = async (newName: string) => {
await api.updateUser(user.id, { name: newName })
}
return (
<InlineEditableField
id="user-name"
label="Name:"
value={user.name}
onSave={handleSave}
placeholder="Enter name..."
/>
)
}Composing with Primitives
Build custom editables using the primitives:
tsx
import { InlineEditable } from '@/shared/ui/components/editable/InlineEditable'
import { EditableTrigger } from '@/shared/ui/components/editable/EditableTrigger'
import { EditableActions } from '@/shared/ui/components/editable/EditableActions'
import { Input } from '@/shadcn/components/ui/input'
export function CustomEditable({ value, onSave }) {
return (
<InlineEditable
value={value}
onSave={onSave}
renderDisplay={({ value, onStartEdit }) => (
<EditableTrigger onClick={onStartEdit}>
<span className="text-lg font-bold">{value}</span>
</EditableTrigger>
)}
renderEditor={({ value, onChange, onSave, onCancel, isSaving, inputProps }) => (
<div className="flex items-center gap-2">
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
autoFocus
/>
<EditableActions
onSave={onSave}
onCancel={onCancel}
isSaving={isSaving}
/>
</div>
)}
/>
)
}Features in Detail
1. Keyboard Shortcuts
All components support Enter/Escape shortcuts by default:
- Enter: Save changes
- Escape: Cancel editing
tsx
<InlineEditableField
value={value}
onSave={handleSave}
// Keyboard shortcuts enabled by default
/>
// Disable if needed
<InlineEditable
value={value}
onSave={handleSave}
enableKeyboardShortcuts={false}
renderDisplay={...}
renderEditor={...}
/>2. Async Save with Loading State
Handle async operations with built-in loading states:
tsx
<InlineEditableField
value={user.email}
onSave={async (newEmail) => {
// Loading state automatically shown
await api.updateEmail(user.id, newEmail)
// Loading state automatically cleared
}}
/>3. Error Handling
Display errors from failed save operations:
tsx
<InlineEditable
value={value}
onSave={async (value) => {
const result = await api.update(value)
if (!result.ok) throw new Error('Update failed')
}}
onSaveError={(error) => {
toast.error(error.message)
}}
renderDisplay={...}
renderEditor={({ error, ...props }) => (
<div>
<input {...props} />
{error && <p className="text-destructive text-sm">{error.message}</p>}
</div>
)}
/>4. Save on Blur
Automatically save when the input loses focus:
tsx
<InlineEditableField
value={value}
onSave={handleSave}
saveOnBlur
hideActions // Hide save/cancel buttons
/>5. Custom Display
Customize how the value is displayed:
tsx
<InlineEditableField
value={user.name}
onSave={handleSave}
renderDisplay={(startEdit) => (
<div className="flex items-center gap-2" onClick={startEdit}>
<Avatar src={user.avatar} />
<span className="font-bold">{user.name}</span>
<Badge>Pro</Badge>
</div>
)}
/>6. Custom Input
Use custom input components:
tsx
<InlineEditableField
value={description}
onSave={handleSave}
renderInput={({ value, onChange, onKeyDown, autoFocus }) => (
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
autoFocus={autoFocus}
rows={4}
/>
)}
/>7. Edited Badge
Show when a field has been modified:
tsx
<InlineEditableField
value={user.name}
onSave={handleSave}
edited={user.name !== originalName}
showEditedBadge
editedBadgeContent="Modified"
/>Common Patterns
Profile Field
tsx
function ProfileField({ label, value, onSave }) {
return (
<div className="flex items-center gap-4 py-2">
<span className="w-32 text-sm text-muted-foreground">{label}</span>
<InlineEditableField
id={`profile-${label}`}
value={value}
onSave={onSave}
className="flex-1"
/>
</div>
)
}
// Usage
<ProfileField label="Name" value={user.name} onSave={updateName} />
<ProfileField label="Email" value={user.email} onSave={updateEmail} />
<ProfileField label="Phone" value={user.phone} onSave={updatePhone} />Inline Title Editor
tsx
function PageTitle({ title, onUpdate }) {
return (
<InlineEditable
value={title}
onSave={onUpdate}
renderDisplay={({ value, onStartEdit }) => (
<EditableTrigger onClick={onStartEdit}>
<h1 className="text-3xl font-bold">{value}</h1>
</EditableTrigger>
)}
renderEditor={({ value, onChange, onSave, onCancel, inputProps }) => (
<div className="flex items-center gap-2">
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
className="text-3xl font-bold"
{...inputProps}
autoFocus
/>
<EditableActions onSave={onSave} onCancel={onCancel} />
</div>
)}
/>
)
}Quick Edit on Blur
tsx
function QuickEditField({ value, onSave }) {
return (
<InlineEditableField
id="quick-edit"
value={value}
onSave={onSave}
saveOnBlur
hideActions
placeholder="Click to edit..."
/>
)
}Multi-line Text
tsx
function DescriptionEditor({ description, onSave }) {
return (
<InlineEditableField
id="description"
label="Description"
value={description}
onSave={onSave}
renderInput={({ value, onChange, onKeyDown, autoFocus }) => (
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
// Prevent Enter from saving for multi-line
if (e.key === 'Enter' && !e.shiftKey) {
e.stopPropagation()
}
onKeyDown(e)
}}
autoFocus={autoFocus}
rows={4}
/>
)}
/>
)
}Best Practices
✅ Do's
- Use InlineEditableField for standard form fields
- Compose primitives for custom UI requirements
- Handle errors with
onSaveErrorcallback - Provide feedback with loading states and toasts
- Use keyboard shortcuts for better UX
- Validate input before saving
- Show "edited" badge for unsaved changes
❌ Don'ts
- Don't use for complex forms - Use a modal or dedicated form page
- Don't forget error handling - Always handle save failures
- Don't skip loading states - Users need feedback
- Don't use for multi-field edits - Edit one field at a time
- Don't forget accessibility - Ensure keyboard navigation works
Accessibility
All components include proper accessibility features:
- Keyboard navigation: Tab, Enter, Escape
- Focus management: Auto-focus on edit mode
- ARIA labels: Proper labeling for screen readers
- Visual feedback: Clear hover and focus states
tsx
<InlineEditableField
id="accessible-field"
label="Name" // Provides context
value={value}
onSave={handleSave}
// Keyboard shortcuts enabled by default
// Auto-focus on edit mode
/>API Reference
For detailed prop documentation, see:
Related Components
- EditableTableCell - Table-specific inline editing
- DataTable - Use with EditableTableCell for editable tables
See Also
- useInlineEdit Hook - Core state management
- Inline Editing Pattern - Best practices and patterns
- Form Components - For complex forms