Product
Settings section
Settings sections, rows, fields, notices, and destructive-action buttons.
$ npx context-ds add settings-sectionUsage
Use it for
Settings pages and panels.
Avoid
- Bold section titles
- A save button per switch
Example
examples/settings-section.tsx
"use client";import * as React from "react";import { SettingsDangerButton, SettingsField, SettingsNotice, SettingsRow, SettingsSection } from "@/components/product/settings-section";import { Button } from "@/components/ui/button";import { HoldButton } from "@/components/ui/hold-button";import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";import { Input } from "@/components/ui/input";import { Switch } from "@/components/ui/switch";export function Product() { const [autoJoin, setAutoJoin] = React.useState(true); const [open, setOpen] = React.useState(false); const [deleted, setDeleted] = React.useState(false); if (deleted) return <div className="grid gap-3"><p role="status">Example organization deleted.</p><Button onClick={() => setDeleted(false)}>Reset example</Button></div>; return ( <div className="flex w-full max-w-2xl flex-col gap-6"> <SettingsSection title="Organization"> <SettingsRow> <SettingsField label="Organization name">{(id) => <Input id={id} defaultValue="Acme" />}</SettingsField> </SettingsRow> </SettingsSection> <SettingsSection title="Members"> <SettingsRow title="Auto-join by domain" description={ <> Anyone signing up with a verified <span className="text-fg">@acme.com</span> email can join as a Member. </> } action={<Switch checked={autoJoin} onCheckedChange={setAutoJoin} aria-label="Auto-join by domain" />} /> <SettingsRow muted title="Enterprise SSO" description="Members sign in through your identity provider." action={<Switch disabled aria-label="Enterprise SSO" />} /> </SettingsSection> <SettingsNotice>Only the Owner can change domain auto-join and Enterprise SSO.</SettingsNotice> <SettingsSection title="Danger zone"> <SettingsRow title="Delete organization" description="Removes every teammate’s access and disables this organization’s API keys." action={<SettingsDangerButton onClick={() => setOpen(true)}>Delete organization</SettingsDangerButton>} /> </SettingsSection> <Dialog open={open} onOpenChange={setOpen}><DialogContent><DialogHeader><DialogTitle>Delete organization?</DialogTitle><DialogDescription>This removes the organization from this example.</DialogDescription></DialogHeader><DialogFooter><Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button><HoldButton label="Hold to delete" onConfirm={() => { setOpen(false); setDeleted(true); }} /></DialogFooter></DialogContent></Dialog> </div> );}Source
components/product/settings-section.tsx
"use client";import * as React from "react";import { cn } from "@/lib/utils";// Settings as the dashboard lays them out: a quiet subtle-gray title just above a card (an aside, such as a link, can// sit at its right), rows inside the card divided by a light line, each with a title, muted text and its control at// the right (stacked under the text on a phone). Fields stack their label over the control; a notice is a card of// muted text for what you can't change here.function SettingsSection({ title, aside, className, children }: { title: React.ReactNode; aside?: React.ReactNode; className?: string; children: React.ReactNode }) { return ( <section data-slot="settings-section" className={cn("flex flex-col gap-2", className)}> <div className="flex items-baseline justify-between gap-4 px-1"> <h3 className="text-body-sm font-normal tracking-normal text-fg-subtle">{title}</h3> {aside} </div> <div className="divide-y divide-line-subtle rounded-card bg-surface px-5 shadow-card">{children}</div> </section> );}function SettingsRow({ title, description, action, muted = false, children }: { title?: React.ReactNode; description?: React.ReactNode; action?: React.ReactNode; muted?: boolean; children?: React.ReactNode }) { return ( <div data-slot="settings-row" className="flex flex-col gap-3 py-4"> <div className="flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:gap-6"> <div className={cn("min-w-0 flex-1", muted && "opacity-50")}> {title && <div className="text-body-sm text-fg">{title}</div>} {description && <div className={cn("text-body-sm text-fg-muted", title && "mt-1")}>{description}</div>} </div> {action} </div> {children} </div> );}/** A field on a settings card: its label (and an aside such as a "Change" link) over the control, given the id. */function SettingsField({ label, aside, children }: { label: React.ReactNode; aside?: React.ReactNode; children: (id: string) => React.ReactNode }) { const id = React.useId(); return ( <div data-slot="settings-field" className="flex flex-col gap-2"> <span className="flex items-center gap-1.5 text-body-sm text-fg"> <label htmlFor={id}>{label}</label> {aside} </span> {children(id)} </div> );}function SettingsNotice({ className, children }: { className?: string; children: React.ReactNode }) { return <div data-slot="settings-notice" className={cn("rounded-card bg-surface p-5 text-body-sm text-fg-muted shadow-card", className)}>{children}</div>;}/** The solid red button a danger zone ends on; the confirming dialog then asks for a hold. */function SettingsDangerButton({ className, ...props }: React.ComponentProps<"button">) { return ( <button type="button" data-slot="settings-danger-button" className={cn( "inline-flex h-9 shrink-0 items-center justify-center rounded-control bg-danger px-4 text-body-sm whitespace-nowrap text-white outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-danger/90 focus-visible:ring-2 focus-visible:ring-danger focus-visible:ring-offset-2 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100", className, )} {...props} /> );}export { SettingsDangerButton, SettingsField, SettingsNotice, SettingsRow, SettingsSection };Reference: context-dashboard c0ddf7a src/components/settings/settings-page.tsx.