Every SaaS app needs a settings page. Here is how to build one with tabs, form sections, and server actions.
Step 1: Settings Layout with Sidebar Navigation
// app/(dashboard)/settings/layout.tsx
import Link from "next/link";
const TABS = [
{ href: "/settings", label: "Profile", icon: "UserIcon" },
{ href: "/settings/notifications", label: "Notifications", icon: "BellIcon" },
{ href: "/settings/billing", label: "Billing", icon: "CreditCardIcon" },
{ href: "/settings/security", label: "Security", icon: "ShieldIcon" },
{ href: "/settings/team", label: "Team", icon: "UsersIcon" },
];
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="mx-auto max-w-5xl py-8">
<h1 className="mb-8 text-2xl font-bold">Settings</h1>
<div className="flex gap-8">
<nav className="w-48 shrink-0">
<ul className="space-y-1">
{TABS.map((tab) => (
<li key={tab.href}>
<Link
href={tab.href}
className="block rounded-lg px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
>
{tab.label}
</Link>
</li>
))}
</ul>
</nav>
<div className="flex-1">{children}</div>
</div>
</div>
);
}
Step 2: Profile Settings Page
// app/(dashboard)/settings/page.tsx
import { ProfileForm } from "@/components/settings/ProfileForm";
import { AvatarUpload } from "@/components/settings/AvatarUpload";
export const metadata = { title: "Profile Settings" };
export default async function ProfileSettingsPage() {
const user = await getCurrentUser();
return (
<div className="space-y-8">
<section>
<h2 className="mb-1 text-lg font-semibold">Profile</h2>
<p className="mb-4 text-sm text-gray-500">
Update your personal information.
</p>
<div className="rounded-xl border p-6 dark:border-gray-700">
<AvatarUpload currentAvatar={user.avatar} />
<ProfileForm user={user} />
</div>
</section>
<section>
<h2 className="mb-1 text-lg font-semibold">Danger Zone</h2>
<p className="mb-4 text-sm text-gray-500">
Irreversible actions.
</p>
<div className="rounded-xl border border-red-200 p-6 dark:border-red-900">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Delete Account</p>
<p className="text-sm text-gray-500">
Permanently delete your account and all data.
</p>
</div>
<button className="rounded-lg border border-red-300 px-4 py-2 text-sm text-red-600 hover:bg-red-50">
Delete Account
</button>
</div>
</div>
</section>
</div>
);
}
Step 3: Profile Form With Server Action
// components/settings/ProfileForm.tsx
"use client";
import { useActionState } from "react";
import { updateProfile } from "@/app/actions";
interface User {
name: string;
email: string;
bio: string;
website: string;
}
export function ProfileForm({ user }: { user: User }) {
const [state, action, pending] = useActionState(updateProfile, null);
return (
<form action={action} className="mt-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium">Name</label>
<input
name="name"
defaultValue={user.name}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Email</label>
<input
name="email"
type="email"
defaultValue={user.email}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Bio</label>
<textarea
name="bio"
defaultValue={user.bio}
rows={3}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Website</label>
<input
name="website"
type="url"
defaultValue={user.website}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
{state?.error && (
<p className="text-sm text-red-600">{state.error}</p>
)}
{state?.success && (
<p className="text-sm text-green-600">Profile updated!</p>
)}
<button
type="submit"
disabled={pending}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{pending ? "Saving..." : "Save Changes"}
</button>
</form>
);
}
Step 4: Notification Settings
// app/(dashboard)/settings/notifications/page.tsx
export default async function NotificationSettings() {
const prefs = await getNotificationPrefs();
return (
<div>
<h2 className="mb-1 text-lg font-semibold">Notifications</h2>
<p className="mb-4 text-sm text-gray-500">
Choose what notifications you receive.
</p>
<div className="space-y-4 rounded-xl border p-6 dark:border-gray-700">
<NotificationToggle
label="Email notifications"
description="Receive email updates about your account."
name="emailNotifications"
defaultChecked={prefs.emailNotifications}
/>
<NotificationToggle
label="Marketing emails"
description="Receive tips, product updates, and offers."
name="marketingEmails"
defaultChecked={prefs.marketingEmails}
/>
<NotificationToggle
label="Weekly digest"
description="Get a weekly summary of your activity."
name="weeklyDigest"
defaultChecked={prefs.weeklyDigest}
/>
</div>
</div>
);
}
function NotificationToggle({
label,
description,
name,
defaultChecked,
}: {
label: string;
description: string;
name: string;
defaultChecked: boolean;
}) {
return (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-gray-500">{description}</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name={name}
defaultChecked={defaultChecked}
className="peer sr-only"
/>
<div className="h-5 w-9 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-all peer-checked:bg-blue-600 peer-checked:after:translate-x-full dark:bg-gray-600" />
</label>
</div>
);
}
Step 5: Security Settings
// app/(dashboard)/settings/security/page.tsx
export default function SecuritySettings() {
return (
<div className="space-y-8">
<section>
<h2 className="mb-1 text-lg font-semibold">Change Password</h2>
<div className="rounded-xl border p-6 dark:border-gray-700">
<form className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium">
Current Password
</label>
<input
type="password"
name="currentPassword"
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
New Password
</label>
<input
type="password"
name="newPassword"
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
Update Password
</button>
</form>
</div>
</section>
<section>
<h2 className="mb-1 text-lg font-semibold">Two-Factor Authentication</h2>
<div className="rounded-xl border p-6 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Not enabled</p>
<p className="text-sm text-gray-500">
Add an extra layer of security to your account.
</p>
</div>
<button className="rounded-lg border px-4 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
Enable 2FA
</button>
</div>
</div>
</section>
<section>
<h2 className="mb-1 text-lg font-semibold">Active Sessions</h2>
<div className="rounded-xl border p-6 dark:border-gray-700">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Chrome on macOS</p>
<p className="text-xs text-gray-500">
Last active: Just now (Current session)
</p>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Safari on iPhone</p>
<p className="text-xs text-gray-500">Last active: 2 hours ago</p>
</div>
<button className="text-xs text-red-600 hover:underline">
Revoke
</button>
</div>
</div>
</div>
</section>
</div>
);
}
Need a Settings Dashboard?
We build web applications with comprehensive settings, user management, and admin panels. Contact us to start your project.