Skip to main content
Back to Blog
Tutorials
4 min read
November 8, 2024

How to Create a Settings Page in Next.js

Build a settings page with tabbed navigation, form sections, and save functionality in Next.js.

Ryel Banfield

Founder & Lead Developer

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.

settings pageuser preferencesNext.jsReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles