Components
Click to edit
Installation
CLI
npx shadcn@latest add "https://diceui.com/r/editable"
Manual
Install the following dependencies:
npm install @radix-ui/react-slot
Copy the composition utilities into your lib/composition.ts
file.
import * as React from "react";
/**
* A utility to compose multiple event handlers into a single event handler.
* Call originalEventHandler first, then ourEventHandler unless prevented.
*/
function composeEventHandlers<E>(
originalEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void,
{ checkForDefaultPrevented = true } = {},
) {
return function handleEvent(event: E) {
originalEventHandler?.(event);
if (
checkForDefaultPrevented === false ||
!(event as unknown as Event).defaultPrevented
) {
return ourEventHandler?.(event);
}
};
}
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
*/
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value.
* This utility takes care of different types of refs: callback refs and RefObject(s).
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}
if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together.
* Accepts callback refs and RefObject(s).
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});
// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
/**
* A custom hook that composes multiple refs.
* Accepts callback refs and RefObject(s).
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(composeRefs(...refs), refs);
}
export { composeEventHandlers, composeRefs, useComposedRefs };
Copy and paste the following code into your project.
"use client";
import { composeEventHandlers, useComposedRefs } from "@/lib/composition";
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
const DATA_ACTION_ATTR = "data-action";
const ROOT_NAME = "Editable";
const AREA_NAME = "EditableArea";
const PREVIEW_NAME = "EditablePreview";
const INPUT_NAME = "EditableInput";
const TRIGGER_NAME = "EditableTrigger";
const LABEL_NAME = "EditableLabel";
const TOOLBAR_NAME = "EditableToolbar";
const CANCEL_NAME = "EditableCancel";
const SUBMIT_NAME = "EditableSubmit";
const EDITABLE_ERROR = {
[ROOT_NAME]: `\`${ROOT_NAME}\` components must be within \`${ROOT_NAME}\``,
[AREA_NAME]: `\`${AREA_NAME}\` must be within \`${ROOT_NAME}\``,
[PREVIEW_NAME]: `\`${PREVIEW_NAME}\` must be within \`${ROOT_NAME}\``,
[INPUT_NAME]: `\`${INPUT_NAME}\` must be within \`${ROOT_NAME}\``,
[TRIGGER_NAME]: `\`${TRIGGER_NAME}\` must be within \`${ROOT_NAME}\``,
[LABEL_NAME]: `\`${LABEL_NAME}\` must be within \`${ROOT_NAME}\``,
[TOOLBAR_NAME]: `\`${TOOLBAR_NAME}\` must be within \`${ROOT_NAME}\``,
[CANCEL_NAME]: `\`${CANCEL_NAME}\` must be within \`${ROOT_NAME}\``,
[SUBMIT_NAME]: `\`${SUBMIT_NAME}\` must be within \`${ROOT_NAME}\``,
} as const;
interface EditableContextValue {
id: string;
inputId: string;
labelId: string;
defaultValue: string;
value: string;
onValueChange: (value: string) => void;
editing: boolean;
onCancel: () => void;
onEdit: () => void;
onSubmit: (value: string) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
dir?: "ltr" | "rtl";
maxLength?: number;
placeholder?: string;
triggerMode: "click" | "dblclick" | "focus";
autosize: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
invalid?: boolean;
}
const EditableContext = React.createContext<EditableContextValue | null>(null);
EditableContext.displayName = ROOT_NAME;
function useEditableContext(name: keyof typeof EDITABLE_ERROR) {
const context = React.useContext(EditableContext);
if (!context) {
throw new Error(EDITABLE_ERROR[name]);
}
return context;
}
type RootElement = React.ComponentRef<typeof Editable>;
interface EditableRootProps
extends Omit<React.ComponentPropsWithoutRef<"div">, "onSubmit"> {
id?: string;
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
defaultEditing?: boolean;
editing?: boolean;
onEditingChange?: (editing: boolean) => void;
onCancel?: () => void;
onEdit?: () => void;
onSubmit?: (value: string) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
dir?: "ltr" | "rtl";
maxLength?: number;
name?: string;
placeholder?: string;
triggerMode?: EditableContextValue["triggerMode"];
asChild?: boolean;
autosize?: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
invalid?: boolean;
}
const EditableRoot = React.forwardRef<HTMLDivElement, EditableRootProps>(
(props, forwardedRef) => {
const {
id = React.useId(),
defaultValue = "",
value: valueProp,
onValueChange: onValueChangeProp,
defaultEditing = false,
editing: editingProp,
onEditingChange: onEditingChangeProp,
onCancel: onCancelProp,
onEdit: onEditProp,
onSubmit: onSubmitProp,
onEscapeKeyDown,
dir = "ltr",
maxLength,
name,
placeholder,
triggerMode = "click",
asChild,
autosize = false,
disabled,
required,
readOnly,
invalid,
className,
...rootProps
} = props;
const inputId = React.useId();
const labelId = React.useId();
const isControlled = valueProp !== undefined;
const [uncontrolledValue, setUncontrolledValue] =
React.useState(defaultValue);
const value = isControlled ? valueProp : uncontrolledValue;
const previousValueRef = React.useRef(value);
const onValueChangeRef = React.useRef(onValueChangeProp);
const isEditingControlled = editingProp !== undefined;
const [uncontrolledEditing, setUncontrolledEditing] =
React.useState(defaultEditing);
const editing = isEditingControlled ? editingProp : uncontrolledEditing;
const onEditingChangeRef = React.useRef(onEditingChangeProp);
React.useEffect(() => {
onValueChangeRef.current = onValueChangeProp;
onEditingChangeRef.current = onEditingChangeProp;
});
const onValueChange = React.useCallback(
(nextValue: string) => {
if (!isControlled) {
setUncontrolledValue(nextValue);
}
onValueChangeRef.current?.(nextValue);
},
[isControlled],
);
const onEditingChange = React.useCallback(
(nextEditing: boolean) => {
if (!isEditingControlled) {
setUncontrolledEditing(nextEditing);
}
onEditingChangeRef.current?.(nextEditing);
},
[isEditingControlled],
);
React.useEffect(() => {
if (isControlled && valueProp !== previousValueRef.current) {
previousValueRef.current = valueProp;
}
}, [isControlled, valueProp]);
const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(
null,
);
const composedRef = useComposedRefs(forwardedRef, (node) =>
setFormTrigger(node),
);
const isFormControl = formTrigger ? !!formTrigger.closest("form") : true;
const onCancel = React.useCallback(() => {
const prevValue = previousValueRef.current;
onValueChange(prevValue);
onEditingChange(false);
onCancelProp?.();
}, [onValueChange, onCancelProp, onEditingChange]);
const onEdit = React.useCallback(() => {
previousValueRef.current = value;
onEditingChange(true);
onEditProp?.();
}, [value, onEditProp, onEditingChange]);
const onSubmit = React.useCallback(
(newValue: string) => {
onValueChange(newValue);
onEditingChange(false);
onSubmitProp?.(newValue);
},
[onValueChange, onSubmitProp, onEditingChange],
);
const contextValue = React.useMemo<EditableContextValue>(
() => ({
id,
inputId,
labelId,
defaultValue,
value,
onValueChange,
editing,
onSubmit,
onEdit,
onCancel,
onEscapeKeyDown,
maxLength,
placeholder,
triggerMode,
autosize,
disabled,
readOnly,
required,
invalid,
}),
[
id,
inputId,
labelId,
defaultValue,
value,
onValueChange,
editing,
onSubmit,
onCancel,
onEdit,
onEscapeKeyDown,
maxLength,
placeholder,
triggerMode,
autosize,
disabled,
required,
readOnly,
invalid,
],
);
const RootSlot = asChild ? Slot : "div";
return (
<EditableContext.Provider value={contextValue}>
<RootSlot
data-slot="editable"
dir={dir}
{...rootProps}
ref={composedRef}
className={cn("flex min-w-0 flex-col gap-2", className)}
/>
{isFormControl && (
<VisuallyHiddenInput
control={formTrigger}
type="hidden"
name={name}
value={value}
disabled={disabled}
readOnly={readOnly}
required={required}
/>
)}
</EditableContext.Provider>
);
},
);
EditableRoot.displayName = ROOT_NAME;
interface EditableLabelProps extends React.ComponentPropsWithoutRef<"label"> {
asChild?: boolean;
}
const EditableLabel = React.forwardRef<HTMLLabelElement, EditableLabelProps>(
(props, forwardedRef) => {
const { asChild, className, children, ...labelProps } = props;
const context = useEditableContext(LABEL_NAME);
const LabelSlot = asChild ? Slot : "label";
return (
<LabelSlot
data-disabled={context.disabled ? "" : undefined}
data-invalid={context.invalid ? "" : undefined}
data-required={context.required ? "" : undefined}
data-slot="editable-label"
{...labelProps}
ref={forwardedRef}
id={context.labelId}
htmlFor={context.inputId}
className={cn(
"font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 data-required:after:ml-0.5 data-required:after:text-destructive data-required:after:content-['*']",
className,
)}
>
{children}
</LabelSlot>
);
},
);
EditableLabel.displayName = LABEL_NAME;
interface EditableAreaProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
const EditableArea = React.forwardRef<HTMLDivElement, EditableAreaProps>(
(props, forwardedRef) => {
const { asChild, className, ...areaProps } = props;
const context = useEditableContext(AREA_NAME);
const AreaSlot = asChild ? Slot : "div";
return (
<AreaSlot
role="group"
data-disabled={context.disabled ? "" : undefined}
data-editing={context.editing ? "" : undefined}
data-slot="editable-area"
{...areaProps}
ref={forwardedRef}
className={cn(
"relative inline-block min-w-0 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className,
)}
/>
);
},
);
EditableArea.displayName = AREA_NAME;
interface EditablePreviewProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
const EditablePreview = React.forwardRef<HTMLDivElement, EditablePreviewProps>(
(props, forwardedRef) => {
const { asChild, className, ...previewProps } = props;
const context = useEditableContext(PREVIEW_NAME);
const onTrigger = React.useCallback(() => {
if (context.disabled || context.readOnly) return;
context.onEdit();
}, [context.disabled, context.readOnly, context.onEdit]);
const PreviewSlot = asChild ? Slot : "div";
if (context.editing || context.readOnly) return null;
return (
<PreviewSlot
role="button"
aria-disabled={context.disabled || context.readOnly}
data-empty={!context.value ? "" : undefined}
data-disabled={context.disabled ? "" : undefined}
data-readonly={context.readOnly ? "" : undefined}
data-slot="editable-preview"
tabIndex={context.disabled || context.readOnly ? undefined : 0}
{...previewProps}
ref={forwardedRef}
onClick={composeEventHandlers(
previewProps.onClick,
context.triggerMode === "click" ? onTrigger : undefined,
)}
onDoubleClick={composeEventHandlers(
previewProps.onDoubleClick,
context.triggerMode === "dblclick" ? onTrigger : undefined,
)}
onFocus={composeEventHandlers(
previewProps.onFocus,
context.triggerMode === "focus" ? onTrigger : undefined,
)}
className={cn(
"cursor-text truncate rounded-sm border border-transparent py-1 text-base focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring data-disabled:cursor-not-allowed data-readonly:cursor-default data-empty:text-muted-foreground data-disabled:opacity-50 md:text-sm",
className,
)}
>
{context.value || context.placeholder}
</PreviewSlot>
);
},
);
EditablePreview.displayName = PREVIEW_NAME;
type InputElement = React.ComponentRef<typeof EditableInput>;
interface EditableInputProps extends React.ComponentPropsWithoutRef<"input"> {
asChild?: boolean;
maxLength?: number;
}
const EditableInput = React.forwardRef<HTMLInputElement, EditableInputProps>(
(props, forwardedRef) => {
const {
asChild,
className,
disabled,
readOnly,
required,
maxLength,
...inputProps
} = props;
const context = useEditableContext(INPUT_NAME);
const inputRef = React.useRef<InputElement>(null);
const composedRef = useComposedRefs(forwardedRef, inputRef);
const isDisabled = disabled || context.disabled;
const isReadOnly = readOnly || context.readOnly;
const isRequired = required || context.required;
const onAutosize = React.useCallback(
(target: HTMLInputElement | HTMLTextAreaElement) => {
if (!context.autosize) return;
if (target instanceof HTMLTextAreaElement) {
target.style.height = "0";
target.style.height = `${target.scrollHeight}px`;
} else {
target.style.width = "0";
target.style.width = `${target.scrollWidth + 4}px`;
}
},
[context.autosize],
);
const onBlur = React.useCallback(
(event: React.FocusEvent<InputElement>) => {
if (isReadOnly) return;
const relatedTarget = event.relatedTarget;
const isAction =
relatedTarget instanceof HTMLElement &&
relatedTarget.closest(`[${DATA_ACTION_ATTR}=""]`);
if (!isAction) {
context.onSubmit(context.value);
}
},
[context.value, context.onSubmit, isReadOnly],
);
const onChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (isReadOnly) return;
context.onValueChange(event.target.value);
onAutosize(event.target);
},
[context.onValueChange, isReadOnly, onAutosize],
);
const onKeyDown = React.useCallback(
(event: React.KeyboardEvent<InputElement>) => {
if (isReadOnly) return;
if (event.key === "Escape") {
const nativeEvent = event.nativeEvent;
if (context.onEscapeKeyDown) {
context.onEscapeKeyDown(nativeEvent);
if (nativeEvent.defaultPrevented) return;
}
context.onCancel();
} else if (event.key === "Enter") {
context.onSubmit(context.value);
}
},
[
context.value,
context.onSubmit,
context.onCancel,
context.onEscapeKeyDown,
isReadOnly,
],
);
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
useIsomorphicLayoutEffect(() => {
if (!context.editing || isReadOnly || !inputRef.current) return;
const frameId = window.requestAnimationFrame(() => {
if (!inputRef.current) return;
inputRef.current.focus();
inputRef.current.select();
onAutosize(inputRef.current);
});
return () => {
window.cancelAnimationFrame(frameId);
};
}, [context.editing, isReadOnly, onAutosize]);
const InputSlot = asChild ? Slot : "input";
if (!context.editing && !isReadOnly) return null;
return (
<InputSlot
aria-required={isRequired}
aria-invalid={context.invalid}
data-slot="editable-input"
disabled={isDisabled}
readOnly={isReadOnly}
required={isRequired}
{...inputProps}
id={context.inputId}
aria-labelledby={context.labelId}
ref={composedRef}
maxLength={maxLength}
placeholder={context.placeholder}
value={context.value}
onBlur={composeEventHandlers(inputProps.onBlur, onBlur)}
onChange={composeEventHandlers(inputProps.onChange, onChange)}
onKeyDown={composeEventHandlers(inputProps.onKeyDown, onKeyDown)}
className={cn(
"flex rounded-sm border border-input bg-transparent py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
context.autosize ? "w-auto" : "w-full",
className,
)}
/>
);
},
);
EditableInput.displayName = INPUT_NAME;
interface EditableTriggerProps
extends React.ComponentPropsWithoutRef<"button"> {
asChild?: boolean;
forceMount?: boolean;
}
const EditableTrigger = React.forwardRef<
HTMLButtonElement,
EditableTriggerProps
>((props, forwardedRef) => {
const { asChild, forceMount = false, ...triggerProps } = props;
const context = useEditableContext(TRIGGER_NAME);
const onTrigger = React.useCallback(() => {
if (context.disabled || context.readOnly) return;
context.onEdit();
}, [context.disabled, context.readOnly, context.onEdit]);
const TriggerSlot = asChild ? Slot : "button";
if (!forceMount && (context.editing || context.readOnly)) return null;
return (
<TriggerSlot
type="button"
aria-controls={context.id}
aria-disabled={context.disabled || context.readOnly}
data-disabled={context.disabled ? "" : undefined}
data-readonly={context.readOnly ? "" : undefined}
data-slot="editable-trigger"
{...triggerProps}
ref={forwardedRef}
onClick={context.triggerMode === "click" ? onTrigger : undefined}
onDoubleClick={context.triggerMode === "dblclick" ? onTrigger : undefined}
/>
);
});
EditableTrigger.displayName = TRIGGER_NAME;
interface EditableToolbarProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
orientation?: "horizontal" | "vertical";
}
const EditableToolbar = React.forwardRef<HTMLDivElement, EditableToolbarProps>(
(props, forwardedRef) => {
const {
asChild,
className,
orientation = "horizontal",
...toolbarProps
} = props;
const context = useEditableContext(TOOLBAR_NAME);
const ToolbarSlot = asChild ? Slot : "div";
return (
<ToolbarSlot
role="toolbar"
aria-controls={context.id}
aria-orientation={orientation}
data-slot="editable-toolbar"
{...toolbarProps}
ref={forwardedRef}
className={cn(
"flex items-center gap-2",
orientation === "vertical" && "flex-col",
className,
)}
/>
);
},
);
EditableToolbar.displayName = TOOLBAR_NAME;
interface EditableCancelProps extends React.ComponentPropsWithoutRef<"button"> {
asChild?: boolean;
}
const EditableCancel = React.forwardRef<HTMLButtonElement, EditableCancelProps>(
(props, forwardedRef) => {
const { asChild, ...cancelProps } = props;
const context = useEditableContext(CANCEL_NAME);
const CancelSlot = asChild ? Slot : "button";
if (!context.editing && !context.readOnly) return null;
return (
<CancelSlot
type="button"
aria-controls={context.id}
data-slot="editable-cancel"
{...{ [DATA_ACTION_ATTR]: "" }}
{...cancelProps}
onClick={composeEventHandlers(cancelProps.onClick, () => {
context.onCancel();
})}
ref={forwardedRef}
/>
);
},
);
EditableCancel.displayName = CANCEL_NAME;
interface EditableSubmitProps extends React.ComponentPropsWithoutRef<"button"> {
asChild?: boolean;
}
const EditableSubmit = React.forwardRef<HTMLButtonElement, EditableSubmitProps>(
(props, forwardedRef) => {
const { asChild, ...submitProps } = props;
const context = useEditableContext(SUBMIT_NAME);
const SubmitSlot = asChild ? Slot : "button";
if (!context.editing && !context.readOnly) return null;
return (
<SubmitSlot
type="button"
aria-controls={context.id}
data-slot="editable-submit"
{...{ [DATA_ACTION_ATTR]: "" }}
{...submitProps}
ref={forwardedRef}
onClick={composeEventHandlers(submitProps.onClick, () => {
context.onSubmit(context.value);
})}
/>
);
},
);
EditableSubmit.displayName = SUBMIT_NAME;
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/src/Checkbox.tsx#L165-L212
*/
interface VisuallyHiddenInputProps<T extends HTMLElement>
extends React.ComponentPropsWithoutRef<"input"> {
control: T | null;
bubbles?: boolean;
}
function VisuallyHiddenInput<T extends HTMLElement>(
props: VisuallyHiddenInputProps<T>,
) {
const {
control,
value,
bubbles = true,
type = "hidden",
...inputProps
} = props;
const inputRef = React.useRef<HTMLInputElement>(null);
const previousRef = React.useRef({ value, previous: value });
const previousValue = React.useMemo(() => {
if (inputRef.current?.value !== value) {
previousRef.current.previous = previousRef.current.value;
previousRef.current.value = value;
}
return previousRef.current.previous;
}, [value]);
React.useEffect(() => {
const input = inputRef.current;
if (!input) return;
const inputProto = window.HTMLInputElement.prototype;
const propertyKey = "value";
const eventType = "input";
const currentValue = JSON.stringify(value);
const descriptor = Object.getOwnPropertyDescriptor(
inputProto,
propertyKey,
) as PropertyDescriptor;
const setter = descriptor.set;
if (previousValue !== currentValue && setter) {
const event = new Event(eventType, { bubbles });
setter.call(input, currentValue);
input.dispatchEvent(event);
}
}, [previousValue, value, bubbles]);
return (
<input
type={type}
{...inputProps}
ref={inputRef}
style={{
...props.style,
border: 0,
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: "1px",
margin: "-1px",
overflow: "hidden",
padding: 0,
position: "absolute",
whiteSpace: "nowrap",
width: "1px",
}}
/>
);
}
const Editable = EditableRoot;
const Root = EditableRoot;
const Label = EditableLabel;
const Area = EditableArea;
const Preview = EditablePreview;
const Input = EditableInput;
const Trigger = EditableTrigger;
const Toolbar = EditableToolbar;
const Cancel = EditableCancel;
const Submit = EditableSubmit;
export {
Editable,
EditableLabel,
EditableArea,
EditablePreview,
EditableInput,
EditableToolbar,
EditableCancel,
EditableSubmit,
EditableTrigger,
//
Root,
Label,
Area,
Preview,
Input,
Toolbar,
Cancel,
Submit,
Trigger,
};
Layout
Import the parts, and compose them together.
import * as Editable from "@/components/ui/editable";
<Editable.Root>
<Editable.Label />
<Editable.Area>
<Editable.Preview />
<Editable.Input />
<Editable.Trigger />
</Editable.Area>
<Editable.Trigger />
<Editable.Toolbar>
<Editable.Submit />
<Editable.Cancel />
</Editable.Toolbar>
</Editable.Root>
Examples
With Double Click
Trigger edit mode with double click instead of single click.
Double click to edit
With Autosize
Input that automatically resizes based on content.
Adjust the size of the input with the text inside.
Todo List
Tricks to learn
Ollie
Kickflip
360 flip
540 flip
With Form
Control the editable component in a form.
API Reference
Root
The main container component for editable functionality.
Prop | Type | Default |
---|---|---|
id | string | React.useId() |
defaultValue | string | "" |
value | string | - |
onValueChange | (value: string) => void | - |
defaultEditing | boolean | false |
editing | boolean | - |
onEscapeKeyDown | (event: KeyboardEvent) => void | - |
onCancel | () => void | - |
onEdit | () => void | - |
onSubmit | (value: string) => void | - |
name | string | - |
placeholder | string | - |
triggerMode | "click" | "dblclick" | "focus" | "click" |
autosize | boolean | false |
disabled | boolean | false |
readOnly | boolean | false |
required | boolean | false |
invalid | boolean | false |
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable" |
Label
The label component for the editable field.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-label" |
[data-disabled] | Present when the editable field is disabled |
[data-invalid] | Present when the editable field is invalid |
[data-required] | Present when the editable field is required |
Area
Container for the preview and input components.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-area" |
[data-disabled] | Present when the editable field is disabled |
[data-editing] | Present when the field is in edit mode |
Preview
The preview component that displays the current value.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-preview" |
[data-empty] | Present when the field has no value |
[data-disabled] | Present when the editable field is disabled |
[data-readonly] | Present when the field is read-only |
Input
The input component for editing the value.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-input" |
Trigger
Button to trigger edit mode.
Prop | Type | Default |
---|---|---|
forceMount | boolean | false |
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-trigger" |
[data-disabled] | Present when the editable field is disabled |
[data-readonly] | Present when the field is read-only |
Toolbar
Container for action buttons.
Prop | Type | Default |
---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-toolbar" |
Submit
Button to submit changes.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-submit" |
Cancel
Button to cancel changes.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Data Attribute | Value |
---|---|
[data-slot] | "editable-cancel" |
Accessibility
Keyboard Interactions
Key | Description |
---|---|
Enter | Submits the current value when in edit mode. |
Escape | Cancels editing and reverts to the previous value. |
Tab | Moves focus to the next focusable element. |