Editable
A shadcn-style editable component built with Ark UI primitives.
Hello World
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditableLabel,
EditablePreview,
} from "@/components/ui/editable";
const EditableBasicDemo = () => (
<Editable defaultValue="Hello World" placeholder="Enter text…">
<EditableLabel>Label</EditableLabel>
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
</Editable>
);
export default EditableBasicDemo;
Installation
npx shadcn@latest add @ark-cn/editableInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/editable.tsx
"use client";
import {
Editable as EditablePrimitive,
type EditableRootProviderProps,
} from "@ark-ui/react/editable";
import type { ComponentProps } from "react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export const Editable = ({
className,
selectOnFocus = false,
...props
}: EditablePrimitive.RootProps) => {
return (
<EditablePrimitive.Root
className={cn(
"flex w-full max-w-md flex-col gap-1.5 text-foreground",
className,
)}
data-slot="editable"
selectOnFocus={selectOnFocus}
{...props}
/>
);
};
export const EditableLabel = ({
className,
...props
}: EditablePrimitive.LabelProps) => {
return (
<EditablePrimitive.Label
className={cn(
"font-medium text-foreground text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-64",
className,
)}
data-slot="editable-label"
{...props}
/>
);
};
export const EditableArea = ({
className,
style,
...rest
}: EditablePrimitive.AreaProps) => {
return (
<EditablePrimitive.Area
className={cn(
"relative max-w-full min-h-8.5 w-full min-w-0 sm:min-h-7.5",
className,
)}
data-slot="editable-area"
style={{
width: "100%",
display: "grid",
gridTemplateColumns: "minmax(0, 1fr)",
...style,
}}
{...rest}
/>
);
};
const editableFieldShell =
"box-border w-full min-w-0 rounded-lg border px-3 py-2 text-base sm:text-sm";
/** Same fixed block size as native text input — preview must match or swap will jump. */
const editableSingleLineHeight =
"h-8.5 sm:h-7.5 data-autoresize:h-auto data-autoresize:min-h-20";
export const EditableInput = ({
className,
...props
}: EditablePrimitive.InputProps) => {
return (
<EditablePrimitive.Input
className={cn(
editableFieldShell,
"border-input bg-background text-foreground outline-none ring-ring/24 transition-[border-color,box-shadow] duration-150 placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] data-invalid:border-destructive/36 data-invalid:ring-destructive/16 dark:bg-input/32 dark:data-invalid:ring-destructive/24",
editableSingleLineHeight,
"data-autoresize:resize-none data-autoresize:wrap-break-word data-autoresize:field-sizing-content",
className,
)}
data-slot="editable-input"
{...props}
/>
);
};
export const EditablePreview = ({
className,
...props
}: EditablePrimitive.PreviewProps) => {
return (
<EditablePrimitive.Preview
className={cn(
editableFieldShell,
"flex cursor-text items-center overflow-hidden text-foreground outline-none transition-[border-color,box-shadow] duration-150 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/24 not-data-readonly:not-data-disabled:hover:dark:bg-input/32",
"border-transparent",
editableSingleLineHeight,
"text-ellipsis whitespace-nowrap",
"data-placeholder-shown:text-muted-foreground",
"data-autoresize:items-start data-autoresize:justify-start data-autoresize:overflow-visible data-autoresize:wrap-break-word data-autoresize:whitespace-pre-wrap!",
className,
)}
data-slot="editable-preview"
{...props}
/>
);
};
export const EditableControl = ({
className,
...props
}: EditablePrimitive.ControlProps) => {
return (
<EditablePrimitive.Control
className={cn("flex flex-wrap items-center gap-2 pt-0.5", className)}
data-slot="editable-control"
{...props}
/>
);
};
const triggerClass = buttonVariants({ size: "sm", variant: "secondary" });
export const EditableEditTrigger = ({
className,
...props
}: EditablePrimitive.EditTriggerProps) => {
return (
<EditablePrimitive.EditTrigger
className={cn(triggerClass, className)}
data-slot="editable-edit-trigger"
{...props}
/>
);
};
export const EditableSubmitTrigger = ({
className,
...props
}: EditablePrimitive.SubmitTriggerProps) => {
return (
<EditablePrimitive.SubmitTrigger
className={cn(triggerClass, className)}
data-slot="editable-submit-trigger"
{...props}
/>
);
};
export const EditableCancelTrigger = ({
className,
...props
}: EditablePrimitive.CancelTriggerProps) => {
return (
<EditablePrimitive.CancelTrigger
className={cn(
buttonVariants({ size: "sm", variant: "outline" }),
className,
)}
data-slot="editable-cancel-trigger"
{...props}
/>
);
};
export const EditableContext = EditablePrimitive.Context;
export const EditableRootProvider = ({
className,
...props
}: EditableRootProviderProps) => {
return (
<EditablePrimitive.RootProvider
className={cn(
"flex w-full max-w-md flex-col gap-1.5 text-foreground",
className,
)}
data-slot="editable"
{...props}
/>
);
};
export type {
EditableEditChangeDetails,
EditableValueChangeDetails,
} from "@ark-ui/react/editable";
export { useEditable, useEditableContext } from "@ark-ui/react/editable";
export type EditableRootProps = ComponentProps<typeof Editable>;
Update import aliases to match your project setup.
Usage
import * as Editable from "@/components/ui/editable"Read exported parts in src/components/ui/editable.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Hello World
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditableLabel,
EditablePreview,
} from "@/components/ui/editable";
const EditableBasicDemo = () => (
<Editable defaultValue="Hello World" placeholder="Enter text…">
<EditableLabel>Label</EditableLabel>
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
</Editable>
);
export default EditableBasicDemo;
Context Hints
Hello
import {
Editable,
EditableArea,
EditableContext,
EditableControl,
EditableEditTrigger,
EditableInput,
EditableLabel,
EditablePreview,
} from "@/components/ui/editable";
const EditableContextHintsDemo = () => (
<Editable defaultValue="Hello" placeholder="Type…">
<EditableLabel>Context</EditableLabel>
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableContext>
{(ctx) =>
ctx.editing ? (
<p className="text-muted-foreground text-xs">
Enter saves, Esc cancels (with default submit mode).
</p>
) : (
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
)
}
</EditableContext>
</Editable>
);
export default EditableContextHintsDemo;
Controlled
Controlled value
Live: Controlled value
import { useState } from "react";
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditableLabel,
EditablePreview,
} from "@/components/ui/editable";
const EditableControlledDemo = () => {
const [value, setValue] = useState("Controlled value");
return (
<Editable
placeholder="Enter text…"
value={value}
onValueChange={(e) => {
setValue(e.value);
}}
>
<EditableLabel>Controlled</EditableLabel>
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
<p className="text-muted-foreground text-xs tabular-nums">
Live: {value || "∅"}
</p>
</Editable>
);
};
export default EditableControlledDemo;
Textarea
Ark UI editable with a textarea.
import {
Editable,
EditableArea,
EditableCancelTrigger,
EditableContext,
EditableControl,
EditableInput,
EditableLabel,
EditablePreview,
EditableSubmitTrigger,
} from "@/components/ui/editable";
const EditableTextareaDemo = () => (
<Editable
activationMode="dblclick"
autoResize
defaultValue="Ark UI editable with a textarea."
placeholder="Enter a description…"
submitMode="none"
>
<EditableLabel>Textarea</EditableLabel>
<EditableArea>
<EditableInput
asChild
className="leading-relaxed data-autoresize:wrap-break-word"
>
<textarea />
</EditableInput>
<EditablePreview className="leading-relaxed" />
</EditableArea>
<EditableContext>
{(ctx) =>
ctx.editing ? (
<>
<p className="text-muted-foreground text-xs">
⌘ Enter (macOS) or Ctrl+Enter to save
</p>
<EditableControl>
<EditableSubmitTrigger>Save</EditableSubmitTrigger>
<EditableCancelTrigger>Cancel</EditableCancelTrigger>
</EditableControl>
</>
) : null
}
</EditableContext>
</Editable>
);
export default EditableTextareaDemo;
Invalid
Required field
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditableLabel,
EditablePreview,
} from "@/components/ui/editable";
const EditableInvalidDemo = () => (
<Editable defaultValue="" invalid placeholder="Required field">
<EditableLabel>Invalid</EditableLabel>
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
</Editable>
);
export default EditableInvalidDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.