Settings panel
A bordered surface of settings rows, label and help on the left, the control on the right.
npx shadcn@latest add https://ui.goodtabs.co/r/settings-panel.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/settings-panel
Notifications
How and when we get in touch.
One email with everything new in the library.
FrequencyHow often the digest arrives.
Where the digest goes.
Three rows: a Switch, a select-like outline button and an Input.
Without a title
Free, freemium, paid and open source.
Smaller cells, more resources per screen.
A plain frame of switch rows, for a short list of toggles.
Danger zone
Danger zone
These cannot be undone.
Transfer workspaceHand ownership to another member.
Delete workspaceRemoves every collection, member and key.
An outline action and one solid destructive button.
Usage
import { SettingsPanel, SettingsRow } from "@/components/ui/settings-panel"
import { Switch } from "@/components/ui/switch"
<SettingsPanel title="Notifications">
<SettingsRow label="Weekly digest" description="One email a week." htmlFor="digest">
<Switch id="digest" defaultChecked />
</SettingsRow>
</SettingsPanel>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | Title in the bar across the top. Omit for a plain frame. | |
| description | ReactNode | One line under the title. | |
| children | ReactNode | SettingsRow elements. Hairlines are drawn between them. | |
| label | ReactNode | On SettingsRow: the setting name. | |
| description | ReactNode | On SettingsRow: what the setting does, one line. | |
| htmlFor | string | On SettingsRow: id of the control, so the label focuses it. | |
| children | ReactNode | On SettingsRow: the control, on the right from sm up. |
Source
settings-panel/settings-panel.tsx
import * as React from "react";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
type SettingsPanelProps = Omit<React.ComponentProps<"section">, "title"> & {
/** Title in the bar across the top. Omit the title for a plain frame. */
title?: React.ReactNode;
/** One line under the title. */
description?: React.ReactNode;
/** SettingsRow elements. Hairlines are drawn between them. */
children: React.ReactNode;
};
/**
* A bordered surface of settings rows with an optional title bar.
* Server component; wrap it in your own form if the rows should submit together.
*/
export function SettingsPanel({ title, description, className, children, ...props }: SettingsPanelProps) {
return (
<section
data-slot="settings-panel"
className={cn("surface-shine flex w-full flex-col overflow-hidden rounded-lg border bg-card", className)}
{...props}
>
{title && (
<>
<div className="flex min-h-12 flex-col justify-center gap-0.5 px-4 py-2.5">
<h2 className="m-0 text-sm leading-5 font-medium text-foreground">{title}</h2>
{description && <p className="m-0 text-xs text-muted-foreground">{description}</p>}
</div>
<Separator />
</>
)}
<div className="divide-y">{children}</div>
</section>
);
}
type SettingsRowProps = Omit<React.ComponentProps<"div">, "children"> & {
/** The setting name. Becomes a label element when htmlFor is set. */
label: React.ReactNode;
/** What the setting does, one line. */
description?: React.ReactNode;
/** Id of the control, so the label focuses it. */
htmlFor?: string;
/** The control: a Switch, a Select, an Input, a Button. */
children: React.ReactNode;
};
/** One setting: label and description on the left, the control on the right from sm up. */
export function SettingsRow({ label, description, htmlFor, className, children, ...props }: SettingsRowProps) {
const Text = htmlFor ? "label" : "span";
return (
<div
data-slot="settings-row"
className={cn("grid gap-3 px-4 py-3 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center sm:gap-6", className)}
{...props}
>
<div className="flex min-w-0 flex-col gap-0.5">
<Text htmlFor={htmlFor} className="text-sm leading-5 text-foreground">
{label}
</Text>
{description && <span className="text-sm text-muted-foreground">{description}</span>}
</div>
<div className="flex min-w-0 items-center sm:justify-self-end">{children}</div>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/settings-panel/prompt.md, or the Copy AI prompt button above.
When to use
- Preferences and account pages with a handful of toggles and small controls
- A danger zone with one or two destructive actions
- Any list of settings that each fit on one line with their control
When not to use
- Long forms with text fields and a save bar, which want the form section
- Settings that need more than one control per row
- Read-only key and value pairs, which want a description list
Agent notes
- Server component with no state; controls manage their own state or get it from a parent
- Pass htmlFor with the control's id so the label focuses it; buttons do not need it
- Controls sit on the right from sm up and drop under the label below that; keep inputs to a fixed width like w-44
- For a destructive action use a solid bg-destructive text-background button, not the soft destructive variant