# Settings Page

Account settings page with section nav, profile, notification, and danger-zone forms.

## Installation

```bash
npx shadcn@latest add https://ui.uptoolkit.com/r/settings-page.json
```

[Registry JSON](https://ui.uptoolkit.com/r/settings-page.json)

## Preview

```tsx
import Page from "app/settings/page";

export function Preview() {
  return <Page />;
}
```


## Source

### page.tsx

```tsx
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";

import { sampleSettingsProfile } from "@/lib/settings-data";
import { SettingsPanels } from "@/components/settings-panels";

/**
 * Account settings page.
 *
 * The route stays a server component and loads the account record; the section
 * nav and the forms live in `SettingsPanels` on the client. Replace
 * `sampleSettingsProfile` with your own session lookup.
 */
export default function Page() {
  const profile = sampleSettingsProfile;

  return (
    <main className="min-h-svh bg-muted/40">
      <div className="mx-auto flex max-w-5xl flex-col gap-6 px-4 py-8">
        <header className="flex flex-col gap-2">
          <Breadcrumb>
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="#">Account</BreadcrumbLink>
              </BreadcrumbItem>
              <BreadcrumbSeparator />
              <BreadcrumbItem>
                <BreadcrumbPage>Settings</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
          <h1 className="m-0 text-2xl font-semibold tracking-tight">Settings</h1>
          <p className="m-0 text-sm text-muted-foreground">
            Manage how you appear, what we send you, and how to leave.
          </p>
        </header>

        <SettingsPanels profile={profile} />
      </div>
    </main>
  );
}
```


### components/settings-panels.tsx

```tsx
"use client";

import * as React from "react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";

import {
  defaultNotificationPreferences,
  settingsSections,
  type NotificationPreference,
  type SettingsProfile,
  type SettingsSectionId,
} from "@/lib/settings-data";

type SettingsPanelsProps = {
  profile: SettingsProfile;
  notifications?: readonly NotificationPreference[];
};

/**
 * Client half of the settings page: the section nav, the forms, and their local
 * state. Swap the `onSubmit` handlers for server actions and the layout is
 * unchanged.
 */
function SettingsPanels({
  profile,
  notifications = defaultNotificationPreferences,
}: SettingsPanelsProps) {
  const [activeSection, setActiveSection] = React.useState<SettingsSectionId>("profile");
  const section = settingsSections.find((entry) => entry.id === activeSection);

  return (
    <div className="flex flex-col gap-6 lg:flex-row">
      <nav aria-label="Settings sections" className="w-full shrink-0 lg:w-56">
        <ul className="m-0 flex list-none gap-1 p-0 lg:flex-col">
          {settingsSections.map((entry) => (
            <li key={entry.id} className="flex-1 lg:flex-none">
              <Button
                variant={entry.id === activeSection ? "secondary" : "ghost"}
                size="sm"
                aria-current={entry.id === activeSection ? "page" : undefined}
                className="w-full justify-start"
                onClick={() => setActiveSection(entry.id)}
              >
                {entry.label}
              </Button>
            </li>
          ))}
        </ul>
      </nav>

      <div className="flex min-w-0 flex-1 flex-col gap-4">
        {section ? (
          <div className="flex flex-col gap-1">
            <h2 className="m-0 text-lg font-semibold tracking-tight">{section.label}</h2>
            <p className="m-0 text-sm text-muted-foreground">{section.description}</p>
          </div>
        ) : null}

        {activeSection === "profile" ? <ProfilePanel profile={profile} /> : null}
        {activeSection === "notifications" ? (
          <NotificationsPanel preferences={notifications} />
        ) : null}
        {activeSection === "danger" ? <DangerPanel handle={profile.handle} /> : null}
      </div>
    </div>
  );
}

function ProfilePanel({ profile }: { profile: SettingsProfile }) {
  const [saved, setSaved] = React.useState(false);

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        setSaved(true);
      }}
    >
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Public profile</CardTitle>
          <CardDescription>This is what other people see.</CardDescription>
        </CardHeader>

        <CardContent className="flex flex-col gap-5">
          <div className="flex items-center gap-4">
            <Avatar className="size-14">
              <AvatarImage src={profile.avatarUrl} alt="" />
              <AvatarFallback>{profile.initials}</AvatarFallback>
            </Avatar>
            <div className="flex flex-col gap-2">
              <div className="flex items-center gap-2">
                <span className="text-sm font-medium">{profile.name}</span>
                <Badge variant="secondary">{profile.plan}</Badge>
              </div>
              <div className="flex gap-2">
                <Button type="button" variant="outline" size="xs">
                  Upload
                </Button>
                <Button type="button" variant="ghost" size="xs">
                  Remove
                </Button>
              </div>
            </div>
          </div>

          <Separator />

          <div className="grid gap-4 sm:grid-cols-2">
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="settings-name">Name</Label>
              <Input id="settings-name" name="name" defaultValue={profile.name} />
            </div>
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="settings-handle">Handle</Label>
              <Input id="settings-handle" name="handle" defaultValue={profile.handle} />
            </div>
          </div>

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="settings-email">Email</Label>
            <Input
              id="settings-email"
              name="email"
              type="email"
              defaultValue={profile.email}
              aria-describedby="settings-email-hint"
            />
            <p id="settings-email-hint" className="m-0 text-xs text-muted-foreground">
              Changing this sends a confirmation link to the new address.
            </p>
          </div>

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="settings-bio">Bio</Label>
            <Textarea
              id="settings-bio"
              name="bio"
              rows={3}
              defaultValue={profile.bio}
              className="resize-none"
            />
          </div>
        </CardContent>

        <CardFooter className="justify-between gap-3 border-t">
          <p className="m-0 text-sm text-muted-foreground" role="status">
            {saved ? "Saved." : " "}
          </p>
          <div className="flex gap-2">
            <Button type="reset" variant="ghost" size="sm" onClick={() => setSaved(false)}>
              Reset
            </Button>
            <Button type="submit" size="sm">
              Save changes
            </Button>
          </div>
        </CardFooter>
      </Card>
    </form>
  );
}

function NotificationsPanel({ preferences }: { preferences: readonly NotificationPreference[] }) {
  const [enabled, setEnabled] = React.useState(
    () => new Set(preferences.filter((entry) => entry.enabled).map((entry) => entry.id)),
  );

  function toggle(id: string) {
    setEnabled((current) => {
      const next = new Set(current);

      if (!next.delete(id)) {
        next.add(id);
      }

      return next;
    });
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base">Email preferences</CardTitle>
        <CardDescription>Security alerts cannot be fully disabled.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-0">
        {preferences.map((preference, index) => (
          <React.Fragment key={preference.id}>
            {index > 0 ? <Separator className="my-3" /> : null}
            <div className="flex items-start justify-between gap-4">
              <div className="flex flex-col gap-0.5">
                <span className="text-sm font-medium">{preference.label}</span>
                <span className="text-sm text-muted-foreground">{preference.description}</span>
              </div>
              <Button
                variant={enabled.has(preference.id) ? "secondary" : "outline"}
                size="sm"
                role="switch"
                aria-checked={enabled.has(preference.id)}
                onClick={() => toggle(preference.id)}
              >
                {enabled.has(preference.id) ? "On" : "Off"}
              </Button>
            </div>
          </React.Fragment>
        ))}
      </CardContent>
    </Card>
  );
}

function DangerPanel({ handle }: { handle: string }) {
  const [confirmation, setConfirmation] = React.useState("");
  const canDelete = confirmation === handle;

  return (
    <Card className="border-destructive/40">
      <CardHeader>
        <CardTitle className="text-base text-destructive">Delete account</CardTitle>
        <CardDescription>
          This removes your profile, projects, and history. It cannot be undone.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-1.5">
        <Label htmlFor="settings-confirm">
          Type <span className="font-mono">{handle}</span> to confirm
        </Label>
        <Input
          id="settings-confirm"
          value={confirmation}
          autoComplete="off"
          placeholder={handle}
          onChange={(event) => setConfirmation(event.target.value)}
        />
      </CardContent>
      <CardFooter className="justify-end border-t">
        <Button variant="destructive" size="sm" disabled={!canDelete}>
          Delete this account
        </Button>
      </CardFooter>
    </Card>
  );
}

export { SettingsPanels, type SettingsPanelsProps };
```


### lib/settings-data.ts

```ts
export type SettingsSectionId = "profile" | "notifications" | "danger";

export type SettingsSection = {
  id: SettingsSectionId;
  label: string;
  description: string;
};

export type NotificationPreference = {
  id: string;
  label: string;
  description: string;
  enabled: boolean;
};

export type SettingsProfile = {
  name: string;
  handle: string;
  email: string;
  bio: string;
  avatarUrl: string;
  initials: string;
  plan: string;
};

export const settingsSections: readonly SettingsSection[] = [
  {
    id: "profile",
    label: "Profile",
    description: "Your name, handle, and how you show up to other people.",
  },
  {
    id: "notifications",
    label: "Notifications",
    description: "Choose what we email you about.",
  },
  {
    id: "danger",
    label: "Danger zone",
    description: "Irreversible account actions.",
  },
];

export const sampleSettingsProfile: SettingsProfile = {
  name: "Rowan Ellis",
  handle: "rowan",
  email: "rowan@example.com",
  bio: "Design engineer working on tools for other builders.",
  avatarUrl: "https://github.com/shadcn.png",
  initials: "RE",
  plan: "Pro",
};

export const defaultNotificationPreferences: readonly NotificationPreference[] = [
  {
    id: "product-updates",
    label: "Product updates",
    description: "New features and meaningful changes. Roughly monthly.",
    enabled: true,
  },
  {
    id: "security-alerts",
    label: "Security alerts",
    description: "New sign-ins and credential changes. Always worth reading.",
    enabled: true,
  },
  {
    id: "weekly-digest",
    label: "Weekly digest",
    description: "A summary of activity across your workspaces.",
    enabled: false,
  },
  {
    id: "marketing",
    label: "Offers and research",
    description: "Occasional surveys and pricing experiments.",
    enabled: false,
  },
];
```



## Usage

Three settings panels behind a section nav: the public profile form, email preferences, and a
danger zone that makes you type your handle before the delete button turns on.

```sh
npx shadcn@latest add @_cn/settings-page
```

Installs `app/settings/page.tsx` plus the `SettingsPanels` client component and the `settings-data`
defaults.

### Saving

`ProfilePanel` submits a real `<form>`, so swapping the local handler for a server action is a
one-line change and the form keeps working without JavaScript:

```tsx
<form action={updateProfile}>{/* same fields */}</form>
```

Notification toggles are `role="switch"` buttons with `aria-checked`, which keeps them announced
correctly without pulling in a switch primitive.

### Section nav

`settingsSections` in `settings-data.ts` drives both the nav and the heading above each panel, so
adding a section means adding one entry and one branch in `SettingsPanels`. For a nav that survives
a refresh, move `activeSection` into a search param instead of `useState`.

