# Settings Accordion With Nested Sections

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/settings-accordion-with-nested-sections).

- Page, with a live preview: https://shadcn.exchange/r/weinc/settings-accordion-with-nested-sections
- Screenshot: https://shadcn.exchange/shot/weinc/settings-accordion-with-nested-sections/light.png
- Registry item: https://we.inc/r/settings-accordion-with-nested-sections.json
- Listed: 2026-10-29, source last changed 2026-10-29

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/settings-accordion-with-nested-sections.json
```

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

## Files

### `components/we-inc/settings-accordion-with-nested-sections.tsx`

```tsx
import { useState } from "react";

type Row = { id: string; label: string; hint: string; on: boolean };
type Group = { id: string; title: string; summary: string; rows: Row[] };

const groups: Group[] = [
  { id: "notify", title: "Notifications", summary: "Email, push and the weekly digest", rows: [
    { id: "n1", label: "New booking", hint: "Email me the moment a client books a slot", on: true },
    { id: "n2", label: "Cancellations", hint: "Push alert when a booking is cancelled", on: true },
    { id: "n3", label: "Weekly digest", hint: "A Monday summary of last week's sessions", on: false },
  ] },
  { id: "privacy", title: "Privacy", summary: "Who can see your profile and calendar", rows: [
    { id: "p1", label: "Public profile", hint: "Show your page in the studio directory", on: true },
    { id: "p2", label: "Show full calendar", hint: "Let clients see busy times, not just free ones", on: false },
  ] },
  { id: "billing", title: "Billing", summary: "Invoices, receipts and payment reminders", rows: [
    { id: "b1", label: "Send receipts", hint: "Email clients a receipt after each payment", on: true },
    { id: "b2", label: "Overdue reminders", hint: "Nudge clients three days after an unpaid invoice", on: false },
  ] },
];

function Chevron({ open }: { open: boolean }) {
  return (
    <svg viewBox="0 0 20 20" className={`h-5 w-5 shrink-0 text-slate-500 transition-transform duration-200 motion-reduce:transition-none ${open ? "rotate-90" : ""}`} fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
      <path d="M7.5 5l5 5-5 5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function SettingsAccordionWithNestedSections() {
  const [openGroups, setOpenGroups] = useState<string[]>(["notify"]);
  const [values, setValues] = useState<Record<string, boolean>>(() =>
    Object.fromEntries(groups.flatMap((g) => g.rows.map((r) => [r.id, r.on]))),
  );
  const flip = (id: string) => setOpenGroups((g) => (g.includes(id) ? g.filter((x) => x !== id) : [...g, id]));
  return (
    <section className="bg-violet-50 px-5 py-14 sm:py-20">
      <div className="mx-auto max-w-xl rounded-2xl border border-violet-100 bg-white p-2 shadow-sm">
        <div className="px-4 pb-3 pt-4">
          <h2 className="text-lg font-semibold text-slate-900">Settings</h2>
          <p className="text-sm text-slate-600">Juniper Pilates, instructor account</p>
        </div>
        {groups.map((g) => {
          const open = openGroups.includes(g.id);
          const count = g.rows.filter((r) => values[r.id]).length;
          return (
            <div key={g.id} className="border-t border-slate-100">
              <h3>
                <button
                  type="button"
                  id={`nsa-${g.id}`}
                  aria-expanded={open}
                  aria-controls={`nsa-${g.id}-rows`}
                  onClick={() => flip(g.id)}
                  className="flex min-h-[64px] w-full items-center gap-3 rounded-xl px-4 py-3 text-left hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-600"
                >
                  <Chevron open={open} />
                  <span className="flex-1">
                    <span className="block text-[15px] font-semibold text-slate-900">{g.title}</span>
                    <span className="block text-sm text-slate-600">{g.summary}</span>
                  </span>
                  <span className="rounded-full bg-violet-100 px-2.5 py-0.5 text-xs font-medium text-violet-800">{count} on</span>
                </button>
              </h3>
              {open && (
                <ul id={`nsa-${g.id}-rows`} role="region" aria-labelledby={`nsa-${g.id}`} className="mb-2 ml-11 mr-2 border-l border-violet-200">
                  {g.rows.map((r) => (
                    <li key={r.id} className="flex items-center gap-4 py-3 pl-4 pr-2">
                      <span className="flex-1">
                        <span id={`nsa-l-${r.id}`} className="block text-sm font-medium text-slate-900">{r.label}</span>
                        <span className="block text-[13px] text-slate-600">{r.hint}</span>
                      </span>
                      <button
                        type="button"
                        role="switch"
                        aria-checked={values[r.id]}
                        aria-labelledby={`nsa-l-${r.id}`}
                        onClick={() => setValues((v) => ({ ...v, [r.id]: !v[r.id] }))}
                        className={`relative h-6 w-11 shrink-0 rounded-full transition-colors motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-600 focus-visible:ring-offset-2 ${values[r.id] ? "bg-violet-600" : "bg-slate-300"}`}
                      >
                        <span className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform motion-reduce:transition-none ${values[r.id] ? "translate-x-5" : ""}`} />
                      </button>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          );
        })}
      </div>
    </section>
  );
}
```
