Skip to main content

json editor

A form-aware JSON editor field with inline, popover, and expanded editing modes.

Philosophy

JSON editing is common in advanced settings, integration payloads, and admin tooling. A plain textarea is easy to ship but hard to use safely. JsonTextareaField keeps JSON editing in your form flow, adds validation, formatting, and keyboard shortcuts, and supports compact or expanded editing modes without introducing a heavy code editor dependency.

How It's Built

Loading diagram...

Installation

npx @gentleduck/cli add json-editor
npx @gentleduck/cli add json-editor

Usage

import { JsonTextareaField } from '@/components/ui/json-editor'
import { JsonTextareaField } from '@/components/ui/json-editor'
import { FormProvider, useForm } from 'react-hook-form'
 
type FormValues = {
  settings: Record<string, unknown> | null
}
 
const form = useForm<FormValues>({
  defaultValues: {
    settings: {
      theme: 'system',
      notifications: true,
    },
  },
})
 
<FormProvider {...form}>
  <form>
    <JsonTextareaField
      control={form.control}
      name="settings"
      label="Settings"
    />
  </form>
</FormProvider>
import { FormProvider, useForm } from 'react-hook-form'
 
type FormValues = {
  settings: Record<string, unknown> | null
}
 
const form = useForm<FormValues>({
  defaultValues: {
    settings: {
      theme: 'system',
      notifications: true,
    },
  },
})
 
<FormProvider {...form}>
  <form>
    <JsonTextareaField
      control={form.control}
      name="settings"
      label="Settings"
    />
  </form>
</FormProvider>

Examples

Inline + sheet expansion

Default mode renders the editor inline and opens an expanded sheet editor with the Full action.

Popover mode

Use mode="popover" to keep layout compact and open the editor only when needed.

Custom full-editor callback

Set expandMode="callback" to hand off full-screen editing to your own flow.

API Reference

JsonTextareaField

PropTypeDefaultDescription
controlControl<TFieldValues>--React Hook Form control object from useForm
nameFieldPath<TFieldValues>--Field path in your form values
labelstring--Visible label rendered above the editor
descriptionstring--Helper text rendered under the label
classNamestring--Additional CSS classes for the field root
actionsClassNamestring--Additional CSS classes for the action buttons row
isEditablebooleantrueDisables editing and save actions when set to false
allowArraybooleantrueWhen false, only JSON objects are accepted (arrays rejected)
mode'inline' | 'popover''inline'Presentation mode for the editor
rowsnumber12Number of rows for the inline textarea
placeholderstring'{\n "theme": "dark"\n}'Placeholder content shown when empty
lineNumbersbooleantrueShows or hides the line-number gutter
lineHeightPxnumber20Line-height used by textarea and line-number gutter
dir'ltr' | 'rtl'--Text direction. Resolved by primitives useDirection (dir prop -> DirectionProvider -> 'ltr'). Controls editor chrome (gutter/buttons); JSON content stays LTR.
langstring--BCP 47 locale tag (e.g. 'ar', 'fa'). Controls the numeral system used for line numbers.
expandMode'none' | 'callback' | 'sheet''sheet'Full-editor behavior for the Full action
sheetSide'left' | 'right''right'Side used when expandMode="sheet"
sheetTitlestring'Edit JSON'Title displayed in the sheet header
textJsonEditorTextSee belowObject of translatable UI strings for i18n support
onExpandEditor(payload: JsonEditorExpandPayload<TFieldValues>) => void--Callback invoked when expandMode="callback" and Full is pressed

JsonEditorText

All fields are optional. Omitted keys fall back to their English defaults.

type JsonEditorText = {
  format?: string        // "Format"
  cancel?: string        // "Cancel"
  save?: string          // "Save"
  full?: string          // "Full"
  close?: string         // "Close"
  keepEditing?: string   // "Keep editing"
  discard?: string       // "Discard"
  discardTitle?: string  // "Discard changes?"
  discardDescription?: string // "You have unsaved changes..."
  statusHint?: string    // "Ctrl/Cmd + Enter: Save, Esc: Cancel"
  sheetStatusHint?: string // "Ctrl/Cmd + Enter: Save, Esc: Close"
  unsavedChanges?: string // "Unsaved changes"
  saved?: string         // "Saved"
  nullPreview?: string   // "NULL"
}
type JsonEditorText = {
  format?: string        // "Format"
  cancel?: string        // "Cancel"
  save?: string          // "Save"
  full?: string          // "Full"
  close?: string         // "Close"
  keepEditing?: string   // "Keep editing"
  discard?: string       // "Discard"
  discardTitle?: string  // "Discard changes?"
  discardDescription?: string // "You have unsaved changes..."
  statusHint?: string    // "Ctrl/Cmd + Enter: Save, Esc: Cancel"
  sheetStatusHint?: string // "Ctrl/Cmd + Enter: Save, Esc: Close"
  unsavedChanges?: string // "Unsaved changes"
  saved?: string         // "Saved"
  nullPreview?: string   // "NULL"
}

JsonEditorExpandPayload

type JsonEditorExpandPayload<TFieldValues extends FieldValues> = {
  name: FieldPath<TFieldValues>
  rawText: string
  value: unknown
}
type JsonEditorExpandPayload<TFieldValues extends FieldValues> = {
  name: FieldPath<TFieldValues>
  rawText: string
  value: unknown
}

Keyboard shortcuts

  • Ctrl/Cmd + Enter saves the current editor buffer.
  • Esc cancels inline edits or closes the expanded sheet.

RTL Support

Set dir="rtl" on JsonEditor for a local override, or set DirectionProvider once at app/root level for global direction. Pass lang for locale-aware line numbers, and use text to translate labels/messages.

See also

  • React Hook Form — Form context components used by JsonTextareaField
  • Textarea — Basic plain-text multiline input
  • Field — Layout primitive for label + description + error composition