Creating a user settings page with editable fields is essential in many web applications. Whether you’re building a social media platform or a SaaS dashboard, giving users control over their personal information enhances the experience. In this guide, we’ll build a modern and responsive user profile page using Next.js and @heroui/react components.
Let’s dive in and create something functional and sleek.
Setting Up the Project

Designing the Profile Card Layout
With the data in place, it’s time to create a layout using Card and Avatar.
//app/user-profile/page.tsx
"use client";
import { Card, Avatar, Input, Button } from "@heroui/react";
import { useState } from "react";
export default function ProfilePage() {
const [user, setUser] = useState({
name: 'Jane Doe',
email: '[email protected]',
avatarUrl: '/images/default-avatar.jpg'
});
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({ ...user });
const handleChange = (e: any) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSave = () => {
setUser({ ...formData });
setIsEditing(false);
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
<Card className="w-full max-w-md p-6 rounded-2xl shadow-md bg-white">
<div className="flex flex-col items-center">
<Avatar
src={user.avatarUrl}
alt={user.name}
className="w-24 h-24 mb-4"
/>
{isEditing ? (
<>
<Input
name="name"
label="Name"
value={formData.name}
onChange={handleChange}
className="mb-4 w-full"
/>
<Input
name="email"
label="Email"
value={formData.email}
onChange={handleChange}
className="mb-4 w-full"
/>
<Button onPress={handleSave} className="w-full" color="primary">
Save Changes
</Button>
</>
) : (
<>
<h2 className="text-xl font-semibold mb-1">{user.name}</h2>
<p className="text-gray-600 mb-4">{user.email}</p>
<Button onPress={() => setIsEditing(true)} className="w-full" color="primary">
Edit Profile
</Button>
</>
)}
</div>
</Card>
</div>
);
}So far, this gives us a card with an avatar and heading. Next, we’ll add inputs for editable fields.
Below the avatar and heading, insert two input fields:
<Input
name="name"
label="Name"
value={formData.name}
onChange={handleChange}
className="mb-4 w-full"
/>
<Input
name="email"
label="Email"
value={formData.email}
onChange={handleChange}
className="mb-4 w-full"
/>Finally, add the save button:
<Button onClick={handleSave} className="w-full" color="primary">
Save Changes
</Button>This layout offers clarity and responsiveness. Users will be able to view and update their info easily.
Adding Interactivity and Finishing Touches
Now that we have the layout, let’s make it interactive. You already have state management in place. What’s missing is proper validation and a smooth user experience.
Updated ProfilePage with Non-Editable userId
1. Disable the Button on No Change
Let’s prevent unnecessary updates. Add a useEffect or conditional to enable the button only if data has changed.
const [isModified, setIsModified] = useState(false);
useEffect(() => {
const changed =
formData.name !== user.name || formData.email !== user.email;
setIsModified(changed);
}, [formData, user]);Then adjust your button.
<Button onPress={handleSave} className="w-full" color="primary" isDisabled={!isModified}>
Save Changes
</Button>2. Improve Feedback
You can add a simple confirmation message:
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter
} from "@heroui/modal";
const [isModalOpen, setIsModalOpen] = useState(false);
const handleSaveConfirmed = () => {
setUser({ ...user, ...formData });
setIsEditing(false);
setIsModalOpen(false);
setTimeout(() => setSaveSuccess(false), 3000);
};
const handleSaveClick = () => {
setIsModalOpen(true);
};Then update your button:
<Button onPress={handleSaveClick} className="w-full" color="primary" isDisabled={!isModified}>
Save Changes
</Button>
{/* Confirmation Modal */}
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalContent>
<ModalHeader>Confirm Save</ModalHeader>
<ModalBody>
Are you sure you want to save your profile changes?
</ModalBody>
<ModalFooter className="flex justify-end gap-2">
<Button variant="ghost" onPress={() => setIsModalOpen(false)} color="danger">
Cancel
</Button>
<Button onPress={handleSaveConfirmed} color="primary">Yes, Save</Button>
</ModalFooter>
</ModalContent>
</Modal>This provides visual feedback and enhances the user experience.
3. Add Alert
You can use the Alert component from HeroUI.
import {Alert} from "@heroui/alert";
<AlertSave isVisible={saveSuccess} />
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalContent>
<ModalHeader>Confirm Save</ModalHeader>
<ModalBody>
Are you sure you want to save your profile changes?
</ModalBody>
<ModalFooter className="flex justify-end gap-2">
<Button variant="ghost" onPress={() => setIsModalOpen(false)} color="danger">
Cancel
</Button>
<Button onPress={handleSaveConfirmed} color="primary">Yes, Save</Button>
</ModalFooter>
</ModalContent>
</Modal>Wrap your page in a layout or add a head tag for SEO optimization.

This ensures that your page is indexed properly and improves discoverability.
Test web application
1. Run the application
npm run dev
2. Access the web page user profile.
http://localhost:3000/user-profile

3. Click the edit button.

The save button is disabled because the data has not been modified.
4. Modify the user name from “Jane Doe” to “John Doe”.

5. Click the Save Changes button.

6. Click the “Yes, Save” button.

7. User name has been changed.

Complete Code
"use client";
import { Card, Avatar, Input, Button, Spacer } from "@heroui/react";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter
} from "@heroui/modal";
import {Alert} from "@heroui/alert";
import { useEffect, useState } from "react";
import React from "react";
export default function ProfilePage() {
const [user, setUser] = useState({
name: 'Jane Doe',
email: '[email protected]',
avatarUrl: '/images/default-avatar.jpg'
});
const [isEditing, setIsEditing] = useState(false);
const [isModified, setIsModified] = useState(false);
const [formData, setFormData] = useState({ ...user });
const [saveSuccess, setSaveSuccess] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleChange = (e: any) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
useEffect(() => {
const changed =
formData.name !== user.name || formData.email !== user.email;
setIsModified(changed);
}, [formData, user]);
const handleSaveConfirmed = () => {
setUser({ ...user, ...formData });
setIsEditing(false);
setIsModalOpen(false);
setIsModified(false);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
};
const handleSaveClick = () => {
setIsModalOpen(true);
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
<Card className="w-full max-w-md p-6 rounded-2xl shadow-md bg-white">
<div className="flex flex-col items-center">
<Avatar
src={user.avatarUrl}
alt={user.name}
className="w-24 h-24 mb-4"
/>
{isEditing ? (
<>
<Input
name="name"
label="Name"
value={formData.name}
onChange={handleChange}
className="mb-4 w-full"
/>
<Input
name="email"
label="Email"
value={formData.email}
onChange={handleChange}
className="mb-4 w-full"
/>
<Button onPress={handleSaveClick} className="w-full" color="primary" isDisabled={!isModified}>
Save Changes
</Button>
</>
) : (
<>
<h2 className="text-xl font-semibold mb-1">{user.name}</h2>
<p className="text-gray-600 mb-4">{user.email}</p>
<Button onPress={() => {setIsEditing(true); setIsModified(false);}} className="w-full" color="primary">
Edit Profile
</Button>
</>
)}
<Spacer y={4} />
<AlertSave isVisible={saveSuccess} />
</div>
</Card>
{/* Confirmation Modal */}
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalContent>
<ModalHeader>Confirm Save</ModalHeader>
<ModalBody>
Are you sure you want to save your profile changes?
</ModalBody>
<ModalFooter className="flex justify-end gap-2">
<Button variant="ghost" onPress={() => setIsModalOpen(false)} color="danger">
Cancel
</Button>
<Button onPress={handleSaveConfirmed} color="primary">Yes, Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
}
interface AlertSaveProps {
isVisible: boolean;
}
const AlertSave: React.FC<AlertSaveProps> = ({ isVisible }) => {
const title = "Success Notification";
const description =
"User profile saved successfully.";
return (
<div className="flex flex-col gap-4">
<Alert
color="success"
description={description}
isVisible={isVisible}
title={title}
variant="faded"
/>
</div>
);
}Conclusion: Fast, Clean, and Extendable
In just a few steps, you’ve built a fully functional user profile page with editable fields using Next.js and HeroUI. The interface is modern, responsive, and clean.
Here’s a quick recap of what we did:
- Set up a Next.js app and installed
@heroui/react. - Built a profile page using
Card,Avatar,Input, andButton. - Managed local state for form fields.
- Improved interactivity with conditionals and feedback messages.
- Optimized UX with Tailwind CSS and semantic HTML.
Finally
By combining HeroUI with Next.js, you get performance, style, and structure out of the box. Developers can focus more on user experience rather than UI boilerplate.
Editable Profile Flow
A Component-Driven Approach with State Management & UX Enhancements
1. Setup & Layout
Initialize a **Next.js** project. Use modular UI components (like HeroUI’s `Card`, `Avatar`, `Input`, `Button`) for a responsive, modern profile card structure.
2. State Logic
Manage two distinct states for profile data and form data to enable easy **cancellation** or **reversion**.
- user: Original, saved data.
- formData: Data actively being edited.
- isEditing: Boolean to toggle UI mode.
3. UI Toggle
Use conditional rendering (`{isEditing ?
{user.name}
)}`}4. Data Persistence
**`handleChange`**: Update `formData` in real-time as the user types. **`handleSaveConfirmed`**: On confirmation, update the main `user` state with the new `formData`, effectively committing the changes.
onSave:
setUser({`{ …user, …formData }`});5. Feedback Loop
Enhance user trust and experience by providing explicit feedback at critical points:
- **Validation:** Disable Save button if no changes are made.
- **Confirmation:** Use a `Modal` before committing data.
- **Success:** Show an `Alert` after successful save.
6. Final Polish
Run the application (`npm run dev`), test the view/edit modes, and confirm the save/cancel flow works correctly. Don’t forget SEO optimization and accessibility checks!
Result:
Functional, Responsive, & Modern Profile Page.
This article was originally published on Medium.



