# Settings panel

> A workspace settings block with name and contact email fields, validation, and save feedback.

A shadcn/ui block from the [@overtrue](https://ui.overtrue.xyz/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/overtrue/settings-panel).

- Page, with a live preview: https://shadcn.exchange/r/overtrue/settings-panel
- Screenshot: https://shadcn.exchange/shot/overtrue/settings-panel/light.png
- Registry item: https://ui.overtrue.xyz/r/settings-panel.json
- Categories: blocks
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/overtrue/settings-panel.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `card`, `input`, `button`

## Notes from the registry

Documentation: https://ui.overtrue.xyz/blocks/settings-panel. Requires an initialized shadcn/ui project with Tailwind CSS v4.

## Files

### `src/registry/overtrue/settings-panel.tsx`

```tsx
"use client";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { SectionCard } from "./section-card";
import { cn } from "@/lib/utils";
export function SettingsPanel({
  initialName = "Acme Studio",
  initialEmail = "team@acme.example",
  onSave,
}: {
  initialName?: string;
  initialEmail?: string;
  onSave?: (values: { name: string; email: string }) => void | Promise<void>;
}) {
  const id = useId();
  const [name, setName] = useState(initialName),
    [email, setEmail] = useState(initialEmail);
  const [status, setStatus] = useState(""),
    [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState({
    name: initialName,
    email: initialEmail,
  });
  const [failed, setFailed] = useState(false);
  const dirty = name !== saved.name || email !== saved.email;
  return (
    <SectionCard
      title="Workspace settings"
      description="Manage your workspace name and contact details."
      contentClassName="p-0"
    >
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (saving || !dirty) return;
          const values = { name, email };
          setSaving(true);
          setStatus("");
          setFailed(false);
          try {
            await onSave?.(values);
            setSaved(values);
            setStatus(
              onSave ? "Changes saved." : "Demo saved locally in this preview.",
            );
          } catch {
            setFailed(true);
            setStatus("Could not save changes. Please try again.");
          } finally {
            setSaving(false);
          }
        }}
      >
        <fieldset disabled={saving} className="min-w-0 space-y-6 p-6">
          <div className="grid gap-2">
            <label htmlFor={`${id}-name`} className="text-sm font-medium">
              Workspace name
            </label>
            <Input
              id={`${id}-name`}
              className="h-10 rounded-md border-input bg-card dark:bg-card text-base shadow-xs focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20 [&:user-invalid]:border-destructive [&:user-invalid]:ring-2 [&:user-invalid]:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:bg-muted dark:disabled:bg-muted sm:text-sm"
              required
              name="workspace-name"
              autoComplete="organization"
              value={name}
              onChange={(event) => {
                setName(event.target.value);
                setStatus("");
                setFailed(false);
              }}
            />
          </div>
          <div className="grid gap-2">
            <label htmlFor={`${id}-email`} className="text-sm font-medium">
              Contact email
            </label>
            <Input
              id={`${id}-email`}
              className="h-10 rounded-md border-input bg-card dark:bg-card text-base shadow-xs focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20 [&:user-invalid]:border-destructive [&:user-invalid]:ring-2 [&:user-invalid]:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:bg-muted dark:disabled:bg-muted sm:text-sm"
              required
              type="email"
              name="contact-email"
              autoComplete="email"
              value={email}
              onChange={(event) => {
                setEmail(event.target.value);
                setStatus("");
                setFailed(false);
              }}
            />
          </div>
        </fieldset>
        <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-muted/30 px-6 py-4">
          <p
            role={failed ? "alert" : "status"}
            className={cn(
              "m-0 min-w-0 grow basis-48 text-xs leading-5 text-muted-foreground",
              failed && "text-destructive",
            )}
          >
            {status ||
              (dirty ? "You have unsaved changes." : "You’re all up to date.")}
          </p>
          <Button
            type="submit"
            className="ml-auto h-10 shrink-0 rounded-md border border-primary bg-primary px-4 text-sm text-primary-foreground shadow-xs hover:bg-primary/90"
            disabled={saving || !dirty}
          >
            {saving ? "Saving…" : "Save changes"}
          </Button>
        </div>
      </form>
    </SectionCard>
  );
}
```

### `src/registry/overtrue/section-card.tsx`

```tsx
import type { ComponentProps, ReactNode } from "react";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";

export interface SectionCardProps extends Omit<
  ComponentProps<typeof Card>,
  "title"
> {
  title?: ReactNode;
  description?: ReactNode;
  actions?: ReactNode;
  contentClassName?: string;
}

/** A shared heading, actions and content surface for admin panels. */
export function SectionCard({
  title,
  description,
  actions,
  children,
  className,
  contentClassName,
  ...props
}: SectionCardProps) {
  return (
    <Card
      data-slot="section-card"
      className={cn(
        "min-w-0 gap-0 rounded-2xl border border-border/70 bg-card py-0 text-card-foreground ring-0",
        "shadow-[0_1px_2px_rgb(0_0_0/0.04),0_8px_24px_-12px_rgb(0_0_0/0.10)] dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.05),0_1px_2px_rgb(0_0_0/0.5)]",
        className,
      )}
      {...props}
    >
      {(title || description || actions) && (
        <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-x-4 gap-y-3 border-b border-border/60 px-5 py-4 [.border-b]:pb-4">
          <div data-slot="section-card-heading" className="min-w-0 flex-1">
            {title && (
              <CardTitle className="m-0 text-sm leading-6 font-semibold tracking-tight text-card-foreground">
                {title}
              </CardTitle>
            )}
            {description && (
              <CardDescription className="m-0 mt-0.5 text-[13px] leading-5 text-muted-foreground">
                {description}
              </CardDescription>
            )}
          </div>
          {actions && (
            <div
              data-slot="section-card-actions"
              className="flex shrink-0 items-center gap-2"
            >
              {actions}
            </div>
          )}
        </CardHeader>
      )}
      <CardContent className={cn("p-5", contentClassName)}>
        {children}
      </CardContent>
    </Card>
  );
}
```
