Inline Edit

inline-edit.tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import { useRef, useState } from "react";
import { Edit01Icon, Tick02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/cn";

function SaveInput() {
  const [isEditing, setIsEditing] = useState(false);
  const [value, setValue] = useState("this.urvish");

  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div className="w-full flex justify-center items-center text-xl">
      <motion.div
        layout
        initial={{
          boxShadow: "0px 0px 2px hsl(var(--foreground) / 0.1)",
        }}
        animate={{
          boxShadow: isEditing
            ? " none border border-foreground"
            : "0px 0px 2px hsl(var(--foreground) / 0.1)",
        }}
        className={cn(
          "flex items-center relative  overflow-hidden border-2 bg-background",
          isEditing &&
            "outline-none ring-2 ring-ring ring-offset-2 ring-offset-background"
        )}
        style={{ borderRadius: 60 }}
      >
        <Input
          ref={inputRef}
          value={value}
          onChange={(e) => setValue(e.target.value)}
          readOnly={!isEditing}
          className={cn(
            "h-12 border-0 shadow-none focus-visible:ring-0 bg-transparent p-0 text-base w-full min-w-32 pl-4 pr-12",
            isEditing ? "text-foreground" : "text-muted-foreground"
          )}
          placeholder="username"
        />
        <AnimatePresence initial={false}>
          {!isEditing ? (
            <motion.span
              key="pen"
              layout="position"
              initial={{ x: 50 }}
              animate={{ x: 0 }}
              exit={{ x: 50 }}
              transition={{ type: "spring", bounce: 0.1 }}
              onClick={() => {
                setIsEditing(true);

                if (inputRef.current) inputRef.current.select();
              }}
              className="absolute right-1 flex items-center justify-center h-10 w-10 rounded-full bg-card/80 border border-[0.2px] hover:bg-card cursor-pointer text-muted-foreground"
            >
              <HugeiconsIcon icon={Edit01Icon} size={20} />
            </motion.span>
          ) : (
            <motion.span
              key="check"
              layout="position"
              initial={{ x: 50 }}
              animate={{ x: 0 }}
              exit={{ x: 50 }}
              transition={{ type: "spring", bounce: 0.1 }}
              onClick={() => setIsEditing(false)}
              className="absolute z-20 right-1 flex items-center justify-center h-10 w-10 rounded-full border-[0.2px]  bg-primary hover:bg-primary/90 cursor-pointer text-primary-foreground"
            >
              <HugeiconsIcon icon={Tick02Icon} size={20} />
            </motion.span>
          )}
        </AnimatePresence>
      </motion.div>
    </div>
  );
}

export default SaveInput;

Installation

npx shadcn@latest add "https://uselayouts.com/r/inline-edit.json"

Install dependencies

npm install motion clsx tailwind-merge @hugeicons/react @hugeicons/core-free-icons

Install UI components

Ensure you have the Input component from Shadcn UI installed.

npx shadcn@latest add input

Copy the code

Copy the code from the Code tab above into components/save-input.tsx.

Usage

import SaveInput from "@/components/inline-edit";

export default function Page() {
  return <SaveInput />;
}

Features

  • Read/Edit Modes: Distinct visual states for viewing and editing content.
  • Seamless Transition: Smooth morphing animation between text display and input field.
  • Contextual Actions: Edit and Save buttons appear only when relevant.
  • Focus Management: Automatically focuses the input field when entering edit mode.
  • Visual Feedback: Ring and shadow effects indicate active editing state.