Source: https://monarchui.dev/components/settings-section/

Patterns · Layout

# SettingsSection

Titled card of settings rows with a save footer and dirty state, loading and no-access states, and a danger-zone variant.

@rookcom/monarch-react

Pattern

## Profile settings

## Usage examples

### Save footer with dirty state

### Danger zone

### Loading and no access

## Purpose

Groups related settings under a heading and description, in a card. SettingsRow lays out one setting and wires its label, description and error to the control; SettingsActionRow lays out one action such as "Delete workspace". The footer can be the standard Save/Discard bar driven by a `dirty` flag.

### Use when

- Settings and preferences pages.
- A form section that saves as a unit and should say when it has unsaved changes.
- A destructive zone (delete, transfer) with `tone="danger"` and SettingsActionRow.

### Avoid when

- Data-entry forms that are not settings: use Field directly.
- Settings that apply immediately (a single switch list): use SwitchSettingsList.

## Design principles

- Label and help on one side, control on the other, so long settings pages scan quickly.
- Each row is a Field, so accessibility wiring is automatic.
- Save/Discard live in the section footer, not per row, and stay disabled until something changed.
- The product owns the draft and the save; the section only shows the state it is given.

## API reference

```tsx
import { SettingsActionRow, SettingsRow, SettingsSection } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `SettingsActionRow` | component | client | `.mn-settings-section` |
| `SettingsRow` | component | client | `.mn-settings-section` |
| `SettingsSection` | component | client | `.mn-settings-section` |

SettingsSection renders a `<section>` labelled by its heading; native attributes pass through.

SettingsRow is a Field: the control child receives the label, description, required, disabled and invalid wiring. `invalid` defaults to true when `error` is given.

With `onSave`, the footer is a polite status line, any `actions`, Discard (`onCancel`) and Save. Without it, the footer is `actions` alone, as before.

SettingsActionRow renders a row with a title, description and an action node; it is not a form field.

Generated from the TypeScript declarations of `packages/react/src/patterns/layout/settings-section`.

## Properties

### `SettingsActionRow`

One action in a settings card: title and consequence on the left, a button on the right. Used for danger-zone items (transfer, archive, delete) where the row is not a form control.

| Prop | Type | Description |
| --- | --- | --- |
| `action` required | `ReactNode` | The button that starts the action, usually opening a ConfirmDialog. |
| `title` required | `ReactNode` | What the action does ("Delete workspace"). |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `description` | `ReactNode` | The consequence, in one or two sentences. |
| `id` | `string` |  |

### `SettingsRow`

One setting: label and description on the left, control on the right.

| Prop | Type | Description |
| --- | --- | --- |
| `children` required | `ReactNode` | The control: Input, Select, Switch, Combobox. It receives the Field ids. |
| `label` required | `ReactNode` | Label of the setting; becomes the control's label. |
| `className` | `string` |  |
| `description` | `ReactNode` |  |
| `disabled` | `boolean` |  |
| `error` | `ReactNode` | Error message rendered under the control. |
| `id` | `string` | Pass `id` to match an existing control id. |
| `invalid` | `boolean` |  |
| `required` | `boolean` |  |

### `SettingsSection`

Titled group of settings in a card, as in Workspace App Settings.

| Prop | Type | Description |
| --- | --- | --- |
| `title` required | `ReactNode` | Section title (h2). |
| `actions` | `ReactNode` | Footer with Save/Cancel actions. |
| `cancelLabel` | `string` | Default "Discard". |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `description` | `ReactNode` |  |
| `dirty` | `boolean` | The draft differs from the saved settings. Shows `dirtyMessage` in the footer (a polite status) and enables Save and Discard. Exposed as `data-dirty`. |
| `dirtyMessage` | `ReactNode` | Status text while `dirty`. Default "You have unsaved changes." |
| `headingLevel` | `"h2" \| "h3"` | Heading level if the section is nested under another h2. |
| `id` | `string` |  |
| `loadingLabel` | `string` | Announced while loading. Default "Loading settings". |
| `noAccessDescription` | `ReactNode` | Explanation under the `no-access` title, such as who can change them. |
| `noAccessTitle` | `ReactNode` | Title of the `no-access` notice. Default "You don’t have access to these settings". |
| `onCancel` | `(() => void)` | Discards the draft. Shown when `onSave` is given. |
| `onSave` | `(() => void)` | Renders the standard footer: a status line, Discard and Save. The product owns the draft; Save calls this, Discard calls `onCancel`. Save and Discard are disabled while the section is not `dirty`. |
| `saveLabel` | `string` | Default "Save changes". |
| `saving` | `boolean` | Save is in flight: Save shows a spinner and Discard is disabled. |
| `state` | `"loading" \| "ready" \| "no-access"` | `loading` shows a skeleton instead of the rows (aria-busy); `no-access` replaces the rows and footer with a locked notice. Default `ready`. Exposed as `data-state`. |
| `stickyFooter` | `boolean` | Keep the footer stuck to the bottom of the scroll area while `dirty`. |
| `tone` | `"danger" \| "default"` | `danger` for destructive zones (delete workspace, transfer ownership). |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `tone` | `'default'``'danger'` | `danger` uses danger border and title colours. Exposed as `data-tone`. |
| `headingLevel` | `'h2'``'h3'` | Heading element for the title. Default `h2`. |
| `stickyFooter` | `'false'``'true'` | While `dirty`, the footer sticks to the bottom of the scroll area. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Dirty | `data-dirty` | Enables Save and Discard, tints the footer and puts `dirtyMessage` in the status line. |
| Saving | `aria-busy` | Save shows a spinner and ignores clicks; Discard is disabled. |
| Loading | `data-state="loading"` | A skeleton replaces the rows; the footer is hidden; the section is busy. |
| No access | `data-state="no-access"` | A locked notice replaces the rows and the footer. |
| Row error | `aria-invalid on the control` | `error` renders a polite live message under the control and marks it invalid. |
| Row required / disabled | – | Passed to the control through Field. |

## Interaction behaviour

- Save calls `onSave`; Discard calls `onCancel`. Both are disabled while the section is not dirty.
- The status line announces "You have unsaved changes." when the section becomes dirty and clears after saving.
- Rows have no behaviour of their own beyond Field wiring; controls behave as usual.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves through the row controls, then Discard and Save. |
| Enter / Space | On Save or Discard, activates it. |

Disabled Save and Discard are skipped by Tab until the section is dirty.

## Accessibility

Role

region (section with aria-labelledby)

Accessible name

The section title.

- Each row label is the control’s accessible name; the description is its accessible description.
- Row errors are announced through `aria-live="polite"`.
- The dirty message is a `role="status"` line, so it is announced without moving focus.
- Loading and saving set `aria-busy` on the section.

## Responsive behaviour

- From 1024px the section header sits beside the card (18rem column); below, it stacks above.
- From 768px each row puts its text and control side by side (control column up to 20rem); below, they stack.
- The footer wraps: the status line takes its own line on narrow screens and the buttons stay together.

## Theme support

- Card, borders, footer, dirty tint and danger colours use Monarch semantic tokens, so every theme applies.

## Do and don't

### Do

- Pass exactly one control per SettingsRow.
- Derive `dirty` from the draft and the saved value, so Discard and Save stay honest.
- Use `tone="danger"` with SettingsActionRow for irreversible actions, each behind a ConfirmDialog.

### Don't

- Do not wrap the control in another Field.
- Do not mix destructive and ordinary settings in one section.
- Do not add a second Save button inside the rows.

## Related components

- [Field](https://monarchui.dev/components/form-field/): The wiring each row uses.
- [SwitchSettingsList](https://monarchui.dev/components/switch-settings-list/): Lists of switches that apply immediately.
- [SectionCard](https://monarchui.dev/components/section-card/): A titled card for content that is not settings.
- [ConfirmDialog](https://monarchui.dev/components/confirm-dialog/): Confirm the actions in a danger zone.
- [PageHeader](https://monarchui.dev/components/page-header/): Title of the settings page.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [EmptyState](https://monarchui.dev/components/empty-state/), [Field](https://monarchui.dev/components/form-field/), [LoadingState](https://monarchui.dev/components/loading-state/) |

## Testing coverage

- Section is a region named by its title; rows wire label, description and required to the control; an error marks it invalid.
- Danger tone is exposed as data-tone.
- The save footer stays disabled until dirty, announces unsaved changes, discards and saves (keyboard).
- Saving shows a busy Save and blocks Discard; custom labels, sticky footer and extra actions.
- Loading and no-access states replace the rows and the footer.
- SettingsActionRow pairs a consequence with its button.
- Renders on the server.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/settings-section; Cross-browser axe: /components/settings-section |

Tests: `packages/react/src/patterns/layout/settings-section`

## All example code

### Profile settings

```tsx
import { Button, Input, SettingsRow, SettingsSection, Switch } from '@rookcom/monarch-react';

<SettingsSection
  title="Profile"
  description="How you appear to your teammates."
  actions={<Button variant="primary">Save</Button>}
>
  <SettingsRow
    label="Display name"
    description="Shown on comments and assignments."
    required
  >
    <Input defaultValue="Ada Lovelace" />
  </SettingsRow>
  <SettingsRow label="Show my status">
    <Switch defaultChecked />
  </SettingsRow>
</SettingsSection>
```

### Save footer with dirty state

```tsx
import { useState } from 'react';
import { Input, SettingsRow, SettingsSection, Switch } from '@rookcom/monarch-react';

function DirtyDemo() {
  const [saved, setSaved] = useState({ name: 'Northfield', digest: true });
  const [draft, setDraft] = useState(saved);
  const [saving, setSaving] = useState(false);
  const dirty = draft.name !== saved.name || draft.digest !== saved.digest;
  return (
    <SettingsSection
      title="Workspace"
      description="Name and notifications for everyone in this workspace."
      dirty={dirty}
      saving={saving}
      onCancel={() => setDraft(saved)}
      onSave={() => {
        setSaving(true);
        setTimeout(() => {
          setSaved(draft);
          setSaving(false);
        }, 800);
      }}
    >
      <SettingsRow label="Workspace name">
        <Input value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} />
      </SettingsRow>
      <SettingsRow label="Weekly digest" description="A summary email every Monday.">
        <Switch
          checked={draft.digest}
          onCheckedChange={(digest) => setDraft({ ...draft, digest })}
        />
      </SettingsRow>
    </SettingsSection>
  );
}

<DirtyDemo />
```

### Danger zone

```tsx
import { Button, SettingsActionRow, SettingsSection } from '@rookcom/monarch-react';

<SettingsSection
  title="Danger zone"
  description="These actions affect everyone in the workspace."
  tone="danger"
>
  <SettingsActionRow
    title="Transfer ownership"
    description="Make another admin the owner. You stay an admin."
    action={<Button>Transfer</Button>}
  />
  <SettingsActionRow
    title="Delete workspace"
    description="Removes every project, file and member. This can’t be undone."
    action={<Button variant="danger">Delete workspace</Button>}
  />
</SettingsSection>
```

### Loading and no access

```tsx
import { SettingsSection } from '@rookcom/monarch-react';

<div style={{ display: 'grid', gap: '1rem', width: '100%' }}>
  <SettingsSection title="Payroll" state="loading">
    {null}
  </SettingsSection>
  <SettingsSection
    title="Security"
    state="no-access"
    noAccessDescription="Ask a workspace admin to change security settings."
  >
    {null}
  </SettingsSection>
</div>
```
