Skip to content

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:

  1. useInlineEdit - Core hook for state management (see hook docs)
  2. InlineEditable - Headless component with render props
  3. EditableTrigger - Reusable trigger button with edit icon
  4. EditableActions - Reusable save/cancel buttons
  5. InlineEditableField - Complete form field implementation
  6. EditableTableCell - Table-specific implementation (see table docs)

This architecture lets you:

  • Start simple with InlineEditableField
  • Customize gradually by composing primitives
  • Go headless with InlineEditable for 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 onSaveError callback
  • 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:



See Also