Xzero UI
Pro library

example settings

Use the public xzero-ui MCP server at https://mcp.xzero.co.uk/mcp. The complete Free and Pro library is available through this service. No account, sign-in, token, or upstream fallback is required. If the service is unavailable, retry later; do not use upstream services.

Request: "Add a settings page where people edit their profile, choose notifications, and can delete their account."

Plan

Job: update profile and notification preferences, or delete the account
Primary action: Save changes
Regions: profile fields (input, select), notifications (switch rows), danger zone (confirm-morph)
States: saving (button loading), saved (button label), save error (inline alert), delete pending and done (confirm-morph)

Choices: switch because each preference applies on its own and reads as on or off; select for time zone (long list); confirm-morph instead of a dialog because the consequence fits in one line.

npx shadcn@4.23.0 add @xzero-ui/input @xzero-ui/select @xzero-ui/switch @xzero-ui/button @xzero-ui/alert @xzero-ui/confirm-morph

settings-form.tsx

"use client";

import { useId, useState, type FormEvent } from "react";
import { Trash2 } from "lucide-react";
import { Alert } from "@/components/xzero/alert/alert";
import { Button } from "@/components/xzero/button/button";
import { ConfirmMorph } from "@/components/xzero/confirm-morph/confirm-morph";
import { Input } from "@/components/xzero/input/input";
import { Select } from "@/components/xzero/select/select";
import { Switch } from "@/components/xzero/switch/switch";
import styles from "./settings.module.css";

type Profile = { name: string; email: string; timeZone: string };
type Props = {
  initial: Profile;
  saveProfile: (profile: Profile) => Promise<void>;
  deleteAccount: () => Promise<void>;
};

const zones = [
  { value: "Europe/Zurich", label: "Zurich (UTC+1)" },
  { value: "America/New_York", label: "New York (UTC-5)" },
  { value: "Asia/Tokyo", label: "Tokyo (UTC+9)" },
];

const preferences = [
  { id: "mentions", title: "Mentions", description: "When someone mentions you in a comment." },
  { id: "digest", title: "Weekly summary", description: "A Monday email with last week's activity." },
];

export function SettingsForm({ initial, saveProfile, deleteAccount }: Props) {
  const id = useId();
  const [profile, setProfile] = useState(initial);
  const [notify, setNotify] = useState<Record<string, boolean>>({ mentions: true, digest: false });
  const [status, setStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
  const dirty = status !== "saving" && JSON.stringify(profile) !== JSON.stringify(initial);

  async function save(event: FormEvent) {
    event.preventDefault();
    setStatus("saving");
    try { await saveProfile(profile); setStatus("saved"); } catch { setStatus("error"); }
  }

  return (
    <div className={styles.stack}>
      <form className={styles.section} onSubmit={save} aria-labelledby={`${id}-profile`}>
        <h2 id={`${id}-profile`} className={styles.heading}>Profile</h2>
        <div className={styles.fields}>
          <Input label="Name" autoComplete="name" value={profile.name}
            onChange={e => { setProfile({ ...profile, name: e.target.value }); setStatus("idle"); }} />
          <Input label="Email" type="email" autoComplete="email" value={profile.email}
            description="We send receipts and sign-in codes here."
            onChange={e => { setProfile({ ...profile, email: e.target.value }); setStatus("idle"); }} />
          <Select label="Time zone" options={zones} value={profile.timeZone}
            onValueChange={timeZone => { setProfile({ ...profile, timeZone }); setStatus("idle"); }} />
        </div>
        {status === "error" && <Alert tone="danger" title="Your changes were not saved">Check your connection and try again.</Alert>}
        <div className={styles.actions}>
          <Button type="submit" loading={status === "saving"} disabled={!dirty && status !== "error"}>
            {status === "saved" ? "Saved" : "Save changes"}
          </Button>
        </div>
      </form>

      <section className={styles.section} aria-labelledby={`${id}-notify`}>
        <h2 id={`${id}-notify`} className={styles.heading}>Notifications</h2>
        <div className={styles.group}>
          {preferences.map(pref => (
            <div key={pref.id} className={styles.row}>
              <div className={styles.rowText}>
                <span id={`${id}-${pref.id}`} className={styles.rowTitle}>{pref.title}</span>
                <span id={`${id}-${pref.id}-hint`} className={styles.rowHint}>{pref.description}</span>
              </div>
              <Switch aria-labelledby={`${id}-${pref.id}`} aria-describedby={`${id}-${pref.id}-hint`}
                checked={notify[pref.id]} onCheckedChange={on => setNotify({ ...notify, [pref.id]: on })} />
            </div>
          ))}
        </div>
      </section>

      <section className={styles.section} aria-labelledby={`${id}-delete`}>
        <h2 id={`${id}-delete`} className={styles.heading}>Delete account</h2>
        <div className={styles.group}>
          <div className={styles.row}>
            <p className={styles.rowHint}>Removes your profile and projects for everyone. This cannot be undone.</p>
            <ConfirmMorph label="Delete account" icon={<Trash2 size={16} strokeWidth={1.75} aria-hidden="true" />}
              prompt="Delete your account?" confirmLabel="Delete" onConfirm={deleteAccount} />
          </div>
        </div>
      </section>
    </div>
  );
}

The page (page.tsx) is a server component: <main className={styles.page}><h1 className={styles.title}>Settings</h1><SettingsForm ... /></main>. This example owns the page container. If your app layout already renders one, keep only max-width: 720px on .page and drop its padding and margin-inline.

settings.module.css

.page { width: 100%; max-width: 720px; margin-inline: auto; padding: 32px clamp(16px, 4vw, 32px); display: grid; gap: 32px; }
.title { margin: 0; font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); color: var(--foreground); }
.stack { display: grid; gap: 40px; }
.section { display: grid; gap: 16px; min-width: 0; }
.heading { margin: 0; font-size: var(--text-lg); font-weight: 500; color: var(--foreground); }
.fields { display: grid; gap: 16px; }
.actions { display: flex; justify-content: flex-end; }
.group { border: 1px solid var(--border); border-radius: var(--radius-surface); background: var(--surface); }
.row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 64px; padding: 16px 24px; }
.row + .row { border-top: 1px solid var(--border-subtle); }
.rowText { display: grid; gap: 2px; min-width: 0; }
.rowTitle { font-size: var(--text-sm); font-weight: 500; color: var(--foreground); }
.rowHint { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }
@media (max-width: 560px) { .row { flex-direction: column; align-items: flex-start; } }

Why it passes the checklist