# Settings Page

> A React account settings page for a back office: profile, account, notifications, billing, security and a danger zone, with a save bar.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add settings-page`
- Docs: https://wingo-ui.com/components/settings-page

## Interaction

Pick a section in the left column (arrow keys move between them) or, with nav="tabs", in the tabs on top; on phones the sections are an iOS style list that pushes the section in with a back button and swipe back. Profile: upload a photo (crop, progress ring, remove), edit the name, title and a bio with a counter, the phone with its country, the language, a searchable time zone and the date format. Account: the email is read only with Verified; Change opens a dialog (a bottom sheet on phones) that asks for the new address and the current password, sends a 6 digit code (123456 passes, the sixth digit submits, Resend code waits 30 s) and then changes it. The workspace address checks itself when you leave the field (Checking…, then Available or Taken; "acme software" gets "Lowercase letters, digits and hyphens only"). Notifications: a switch per event and channel with a column toggle for all (one group per event on phones); switches save at once and flip back with a toast if the save fails. Quiet hours turn on with a switch and take two times (22:00 to 08:00 reads "next day"); the digest is a segmented choice. Billing: the plan with Change plan (a dialog to pick Start, Pro or Business with the monthly total) and Cancel (a confirmation; the plan then shows its end date and a way to resume it), the usage of users, AI tokens and storage, the card with Update, the company details with a CUI check and the address, and the invoices (cards on phones) with a download link each. Security: Change password opens a dialog with the strength meter and rules; Turn on starts a three step wizard: scan the QR code or copy the key, type the code (123456), then copy or download the ten backup codes; the badge On pops in and Turn off asks first. Revoke any other session or all of them at once; the current one cannot be revoked. Danger zone: Export counts up in a toast and then offers the file; Delete account asks you to type DELETE first. Any text change slides up the save bar with Discard and Save (mod+s); a failed save keeps the draft, shakes the bar and shows why, invalid fields turn red and the first one takes focus. Switching sections with unsaved changes asks first.

## Installation

```bash
npx wingo-ui@latest add settings-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/settings-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/settings-page.tsx`
- `components/blocks/settings-page-sections.tsx`
- `components/blocks/settings-page-data.ts`

npm dependencies: `npm i motion lucide-react react-hook-form zod`

Wingo UI dependencies (installed automatically):

- [Settings Layout](https://wingo-ui.com/components/settings-layout) ([Markdown](https://wingo-ui.com/components/settings-layout.md))
- [Avatar Upload](https://wingo-ui.com/components/avatar-upload) ([Markdown](https://wingo-ui.com/components/avatar-upload.md))
- [Address Input](https://wingo-ui.com/components/address-input) ([Markdown](https://wingo-ui.com/components/address-input.md))
- [QR Code](https://wingo-ui.com/components/qr-code) ([Markdown](https://wingo-ui.com/components/qr-code.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Phone Input](https://wingo-ui.com/components/phone-input) ([Markdown](https://wingo-ui.com/components/phone-input.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))
- [Time Picker](https://wingo-ui.com/components/time-picker) ([Markdown](https://wingo-ui.com/components/time-picker.md))
- [Input Group](https://wingo-ui.com/components/input-group) ([Markdown](https://wingo-ui.com/components/input-group.md))
- [Password Input](https://wingo-ui.com/components/password-input) ([Markdown](https://wingo-ui.com/components/password-input.md))
- [OTP Input](https://wingo-ui.com/components/otp-input) ([Markdown](https://wingo-ui.com/components/otp-input.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [List](https://wingo-ui.com/components/list) ([Markdown](https://wingo-ui.com/components/list.md))
- [Usage Meter](https://wingo-ui.com/components/usage-meter) ([Markdown](https://wingo-ui.com/components/usage-meter.md))
- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [Form validation](https://wingo-ui.com/components/form-validation) ([Markdown](https://wingo-ui.com/components/form-validation.md))
- [useCooldown](https://wingo-ui.com/components/use-cooldown) ([Markdown](https://wingo-ui.com/components/use-cooldown.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [File export](https://wingo-ui.com/components/file-export) ([Markdown](https://wingo-ui.com/components/file-export.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
// app/(admin)/settings/page.tsx (the shell lives in app/(admin)/layout.tsx: <AdminShell>{children}</AdminShell>)
"use client"

import { SettingsPage } from "@/components/blocks/settings-page"

export default function SettingsRoute() {
  return (
    <SettingsPage
      onSave={async (section, values) => {
        const res = await fetch(`/api/settings/${section}`, { method: "PUT", body: JSON.stringify(values) })
        if (!res.ok) throw new Error("We could not save. Please try again.")
      }}
      onPreferenceChange={async (preference, value) => {
        await fetch("/api/settings/preferences", { method: "PATCH", body: JSON.stringify({ preference, value }) })
      }}
      onTwoFactorChange={async (enabled) => {
        await fetch("/api/security/2fa", { method: enabled ? "POST" : "DELETE" })
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `section?` | `"profil" \| "cont" \| "notificari" \| "facturare" \| "securitate" \| "pericol"` |  | The open section, controlled; pair it with onSectionChange (a route per section). |
| `defaultSection?` | `"profil" \| "cont" \| "notificari" \| "facturare" \| "securitate" \| "pericol"` | `"profil"` | The section open on mount. |
| `onSectionChange?` | `(section: SettingsPageSection) => void` |  | Called when another section opens (after the unsaved changes question). |
| `sections?` | `SettingsPageSection[]` | `"all six"` | Which sections exist, in this order. |
| `profile?` | `SettingsProfile` |  | Emma Carter's profile: photo, name, title, bio, phone, language, time zone, date format (SETTINGS_PAGE_SAMPLE.profile). |
| `account?` | `SettingsAccount` |  | The email with its verified flag and the workspace name and address (SETTINGS_PAGE_SAMPLE.account). |
| `notifications?` | `SettingsNotificationPrefs` |  | Event by channel switches, quiet hours and the digest (SETTINGS_PAGE_SAMPLE.notifications). |
| `notificationEvents?` | `{ id: string; label: string; description?: string }[]` |  | The rows of the notifications grid: Overdue invoice, Payment received, Deal won, Mention, Task assigned, Weekly report. |
| `channels?` | `("email" \| "push" \| "sms")[]` | `"all three"` | The notification columns. |
| `billing?` | `SettingsBilling` |  | Plan, renewal, seats, AI tokens, storage, the card, the company details and the invoices (SETTINGS_PAGE_SAMPLE.billing). |
| `plans?` | `SettingsPlan[]` |  | The plans offered by Change plan: Start, Pro, Business with the price per user (SETTINGS_PAGE_SAMPLE.plans). |
| `sessions?` | `SettingsSession[]` |  | Signed in devices: MacBook Pro (current), iPhone 14, Windows (SETTINGS_PAGE_SAMPLE.sessions). |
| `timeZones?` | `{ value: string; label: string; suggested?: boolean; keywords?: string[] }[]` |  | The time zone picker's options, suggested ones first (SETTINGS_PAGE_SAMPLE.timeZones). |
| `takenSlugs?` | `string[]` |  | Workspace addresses already in use, for the default availability check. |
| `twoFactorEnabled?` | `boolean` |  | Two-step verification, controlled; pair it with onTwoFactorChange. |
| `defaultTwoFactorEnabled?` | `boolean` | `false` | Two-step verification on at mount (On instead of Turn on). |
| `onTwoFactorChange?` | `(enabled: boolean) => (void \| Promise<unknown>)` |  | Turns two-step verification on (after the code checks out) or off (after the confirmation). Default: a simulated request. |
| `twoFactorSecret?` | `string` | `"JBSWY3DPEHPK3PXP"` | The base32 key shown under the QR code, in groups of four, with a copy button. |
| `twoFactorUri?` | `string` | `"otpauth://totp/Acme%20CRM:ana.pop%40acme.com?secret=JBSWY3DPEHPK3PXP&issuer=Acme%20CRM"` | The otpauth:// address the QR code encodes. |
| `backupCodes?` | `string[]` |  | The ten backup codes of the last step (generated with a fixed seed in the sample). |
| `verifyTwoFactorCode?` | `(code: string) => (boolean \| Promise<boolean>)` |  | Checks the authenticator code. Default: 123456 passes. |
| `passwordChangedAt?` | `Date \| number` |  | When the password last changed ("Last changed on Jun 12, 2026"). |
| `loginAlerts?` | `boolean` | `true` | The starting state of Alerte la autentificare. |
| `verifyEmailCode?` | `(email: string, code: string) => (boolean \| Promise<boolean>)` |  | Checks the code sent to the new email. Default: 123456 passes. |
| `checkWorkspaceAddress?` | `(slug: string) => (boolean \| Promise<boolean>)` |  | Whether a workspace address is free; runs when the field loses focus and before a save. Default: takenSlugs after 600 ms. |
| `nav?` | `"sidebar" \| "tabs"` | `"sidebar"` | The section navigation from 768px: a left column or tabs on top. |
| `saveBarPosition?` | `"sticky" \| "fixed"` | `"sticky"` | Where the save bar sits: at the bottom of the content column, or pinned to the window. Phones always pin it. |
| `saveShortcut?` | `string` | `"mod+s"` | Saves the open section while it has changes; "" turns the key off. |
| `confirmLeave?` | `boolean` | `true` | Asks before switching sections with unsaved changes. |
| `resendSeconds?` | `number` | `30` | How long Retrimite codul waits in the email dialog. |
| `defaultDialog?` | `"none" \| "email" \| "password" \| "two-factor" \| "plan" \| "cancel-plan" \| "delete"` | `"none"` | Opens with this dialog on desktop; the section that owns it has to be the one open (email in cont, password and two-factor in securitate, plan and cancel-plan in facturare, delete in pericol). |
| `defaultProfileDraft?` | `Partial<SettingsProfileDraft>` |  | Edits applied to the profile form on mount, so it opens with unsaved changes. |
| `onSave?` | `(section: SettingsFormSection, values: SettingsSaveValues[SettingsFormSection]) => (void \| Promise<unknown>)` |  | Saves the open section's text fields from the save bar (profil, cont, notificari, facturare). A rejection keeps the draft and shows its message in the bar. Default: a simulated request. |
| `onPreferenceChange?` | `(preference: string, value: boolean) => (void \| Promise<unknown>)` |  | Switches that save on their own: "notificari.matrix.<event>.<channel>", "notificari.matrix.*.<channel>", "notificari.quietHours", "securitate.loginAlerts". A rejection flips the switch back with a toast. |
| `onUpload?` | `(blob: Blob, context: { file; signal; onProgress }) => Promise<string \| void>` |  | Uploads the cropped photo; resolve the new url. Default: a pretend upload with progress that keeps the local preview. |
| `onEmailChange?` | `(email: string, currentPassword: string) => (void \| Promise<unknown>)` |  | Runs after the code for the new address checks out. |
| `onPasswordChange?` | `(values: { currentPassword: string; newPassword: string }) => (void \| Promise<unknown>)` |  | The password dialog's submit; a rejection shows its message in the dialog. |
| `onSessionRevoke?` | `(session: SettingsSession) => (void \| Promise<unknown>)` |  | Signs a device out (once per device for Sign out all others). |
| `onExport?` | `() => (void \| Promise<unknown>)` |  | Export your data. Default: a toast that counts up, then offers a JSON download. |
| `onDeleteAccount?` | `() => (void \| Promise<unknown>)` |  | Runs after the typed confirmation. |
| `onPlanChange?` | `(planId: string \| null) => (void \| Promise<unknown>)` |  | A plan id switches (or resumes); null cancels at the end of the period. |
| `onDirtyChange?` | `(dirty: boolean) => void` |  | The open section has unsaved changes (hide a phone tab bar while true, one bar per edge). |
| `paymentMethodHref?` | `string` | `"/settings/billing/card"` | Where Update on the card goes. |
| `labels?` | `Partial<SettingsPageLabels>` | `"DEFAULT_SETTINGS_PAGE_LABELS"` | Every visible string, merged over the English DEFAULT_SETTINGS_PAGE_LABELS. Nested keys carry the composed items' strings (validation, avatarUpload, cropper, phoneInput, timePicker, select, addressInput, qrCode, table, list, usageMeter). |
| `locale?` | `string` | `"en-US"` | Intl locale for numbers, money, dates and relative times. |
| `currency?` | `string` | `"USD"` | The currency of the plan prices and the invoices. |
| `now?` | `Date \| number` |  | The reference time for the sessions and the date format sample; default the live clock. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Accent of the section navigation, the switches, the primary buttons and the save button. |
| `radius?` | `"sm" \| "md" \| "lg"` | `"md"` | Corner size of the settings groups (12 / 16 / 24px). |
| `loading?` | `boolean` | `false` | The open section as skeleton rows in the real layout. |
| `animateIn?` | `boolean` | `true` | The page rises in once when it mounts on the client; off in thumbnails and under reduced motion. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` | `"<a>"` | The router link for the card and invoice links; default the surrounding LinkProvider's (the admin shell's). |
| `className?` | `string` |  | Classes for the root. |
| `classNames?` | `Partial<Record<"layout" \| "section" \| "saveBar", string>>` |  | Classes for the settings layout, every settings group and the save bar. |
| `failNextSave?` | `boolean` | `false` | The built-in pretend save (no onSave) fails once, so the error in the bar and the kept draft can be seen and tested. |

## Parts

### DEFAULT_SETTINGS_PAGE_LABELS (constant)

Every English string of the page, including the nested strings of the composed items (validation, avatar upload, cropper, phone input, time picker, table). Spread it and override what you translate.

### SETTINGS_PAGE_LABELS_EN (constant)

An alias of DEFAULT_SETTINGS_PAGE_LABELS, kept so imports that name the English set keep working.

### ProfileSection (component)

The Profile section on its own: photo, name, title, bio, phone, language, time zone and date format. Takes { ctx: SettingsSectionContext }, the state SettingsPage builds; use it when you compose your own page.

### AccountSection (component)

The Account section on its own: the verified email with its change dialog and the workspace address check. Takes { ctx }.

### NotificationsSection (component)

The Notifications section on its own: a switch per event and channel, quiet hours and the digest. Takes { ctx }.

### BillingSection (component)

The Billing section on its own: the plan with change and cancel dialogs, usage meters, the card, company details and invoices. Takes { ctx }.

### SecuritySection (component)

The Security section on its own: password change, the two-factor setup wizard with backup codes, and the session list. Takes { ctx }.

### DangerSection (component)

The danger zone on its own: data export with progress and account deletion that asks you to type DELETE. Takes { ctx }.

### SectionSkeleton (component)

The loading rows of a section in its real layout, the same ones loading shows. Takes { ctx, section }.

### SETTINGS_PAGE_SAMPLE (constant)

The demo data the page renders without props: a profile, account, notification preferences, plans, billing, sessions, time zones and two-factor setup. Replace each piece with your own through the matching prop.

### SETTINGS_PAGE_SAMPLE_EN (constant)

An alias of SETTINGS_PAGE_SAMPLE, kept for imports that name the English sample.

### SETTINGS_PAGE_NOW (constant)

The fixed clock of the sample (Thursday, Sep 24, 2026, 4:40 PM), so session times and screenshots never drift.

### SETTINGS_PAGE_SECTIONS (constant)

The section ids in order: profil, cont, notificari, facturare, securitate, pericol. Use them for section and onSectionChange.

### SETTINGS_CHANNELS (constant)

The notification channels of the sample, as columns of the preferences grid: email, push and sms.

### generateBackupCodes (function)

Seeded demo backup codes in the XXXX-XXXX format, the same on every render. For screenshots and tests only; generate real codes on your server.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `count?` | `number` | `10` | How many codes. |
| `seed?` | `number` | `2026` | The random seed; the same seed gives the same codes. |
| `returns?` | `string[]` |  | The codes. |

### totpUri (function)

Builds the otpauth:// URI the two-factor QR code encodes, for an account, a base32 secret and an issuer.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `account` | `string` |  | The account name shown in the authenticator app, usually the email. |
| `secret?` | `string` |  | The base32 secret; defaults to the sample's. |
| `issuer?` | `string` |  | The app name shown in the authenticator; defaults to the sample's. |
| `returns?` | `string` |  | The otpauth://totp/... URI. |

### SettingsSectionContext (type)

The state a section receives as ctx: labels, locale, currency, tone, the data and the save handlers.

## Examples

- **Notifications**: Events by channel with a toggle per column, quiet hours from 22:00 to 08:00 and the digest.
- **Billing**: The Pro plan with its usage, the card, the company details with the address and the invoices.
- **Two-step verification setup**: defaultDialog="two-factor": step 1 of the wizard with the QR code and the key; 123456 passes step 2.
- **Unsaved profile**: defaultProfileDraft changes the name and the title: the save bar offers Discard and Save (mod+s).
- **Delete the account**: defaultDialog="delete": the button unlocks once you type DELETE.
- **Loading**: loading: the section as skeleton rows under the navigation.
- **Brand tone, tabs**: tone="brand", radius="lg", nav="tabs" on Security with two-step verification on.
- **British English**: locale="en-GB": dates and numbers follow the locale.

## See also

- [Settings Layout](https://wingo-ui.com/components/settings-layout) ([Markdown](https://wingo-ui.com/components/settings-layout.md))
- [Admin Shell](https://wingo-ui.com/components/admin-shell) ([Markdown](https://wingo-ui.com/components/admin-shell.md))
- [Avatar Upload](https://wingo-ui.com/components/avatar-upload) ([Markdown](https://wingo-ui.com/components/avatar-upload.md))
- [Password Input](https://wingo-ui.com/components/password-input) ([Markdown](https://wingo-ui.com/components/password-input.md))
- [OTP Input](https://wingo-ui.com/components/otp-input) ([Markdown](https://wingo-ui.com/components/otp-input.md))
- [Users Admin Page](https://wingo-ui.com/components/users-admin-page) ([Markdown](https://wingo-ui.com/components/users-admin-page.md))

---

Canonical: https://wingo-ui.com/components/settings-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
