Settings sectionDesign system
Product

Settings section

Settings sections, rows, fields, notices, and destructive-action buttons.

npx context-ds add settings-section
In the product

Usage

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.