Feedback & Layout Patterns

When to use toast, alert, or modal — plus page shell, sidebar, and responsive layout with GHDS.

Overview

Feedback is how an app tells the user what just happened — a save succeeded, a request failed, or something needs attention. This pattern covers how to choose the right feedback channel (Toast, Alert, Modal) and how to compose a page shell — header, body, footer, sidebar — that responds to the viewport.

Feedback Channel Decision Tree

Choosing the right feedback component is the most frequent pattern decision in any app. Use this tree:

Did the user initiate the action?
├── Yes (they clicked "Save", "Delete", "Submit")
│   └── Is the result still visible on screen?
│       ├── Yes → Alert (inline, persistent)
│       └── No (navigated away / async completion) → Toast

└── No (system event, websocket, background sync)
    └── Does this require a user decision?
        ├── Yes → Modal (interrupting, requires action)
        └── No → Toast (informational, auto-dismiss)

Component Comparison

ToastAlertModal
PositionFixed, bottom-rightInline, in layoutCentred overlay with scrim
PersistenceAuto-dismiss (default 5s)Stays until dismissedStays until explicit close
InterruptsNo (polite, except danger)No (polite, except danger)Yes (traps focus, locks scroll)
Requires actionNoOptional dismiss buttonYes (at minimum “OK” / “Cancel”)
Use for”Saved”, “Copied”, “Message sent”Form errors, status banners, warningsConfirmations, detail views, short forms
StackingMultiple can stackOne per contextOne at a time (never nested)

State Colours

All feedback components use the same severity palette from sys.color.bg.*:

SeverityToken prefixIconWhen
Info*.infoℹ️ infoNeutral status, contextual hints
Success*.success✓ checkTask completed, data saved
Warning*.warning⚠ warningNon-blocking issue, attention needed
Danger*.danger⚠ warningError, destructive action, failure

Do

  • Use Toast for transient confirmations (save, copy, send) — they auto-dismiss so the user is not blocked.
  • Use Alert for inline status that needs to persist (form errors, connection status, maintenance notice).
  • Use Modal for interrupting decisions (delete confirmation, sign-out prompt, critical error).
  • Match the severity to the outcome: success for saves, danger for failures, warning for cautions.

Don't

  • Don't use a Modal for a simple success message — it's too heavy.
  • Don't stack multiple Modals — it disorients users and creates focus-management issues.
  • Don't use Toast for errors that require user action — they disappear before the user can act.
  • Don't rely on colour alone to communicate severity — always include an icon and text.

Page Shell

A page shell is the persistent chrome around content: header, sidebar (optional), body, and footer. GHDS does not ship a dedicated shell component — instead, compose it from existing components and tokens.

┌─────────────────────────────────────────────────────┐
│  Header (Breadcrumb + heading + actions)            │  ← sys.spacing.lg padding
├─────────────────────────────────────────────────────┤
│                                                     │
│  Body (Card / Table / Form — the main content)      │  ← flex: 1; overflow: auto
│                                                     │
├─────────────────────────────────────────────────────┤
│  Footer (Pagination / legal / secondary links)      │  ← sys.spacing.md padding
└─────────────────────────────────────────────────────┘
// React — page shell
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
  <header style={{ padding: 'var(--sys-spacing-lg)' }}>
    <Breadcrumb items={breadcrumbs} />
    <h1>Page Title</h1>
  </header>
  <main style={{ flex: 1, padding: '0 var(--sys-spacing-lg)' }}>
    {children}
  </main>
  <footer style={{ padding: 'var(--sys-spacing-md) var(--sys-spacing-lg)' }}>
    <Pagination current={page} total={totalPages} onChange={setPage} />
  </footer>
</div>

A two-column layout with a fixed-width sidebar (Card or Menu) and a fluid body. Use sys.breakpoint.tablet to collapse the sidebar into a drawer or Tabs on mobile.

┌──────────┬──────────────────────────────────────────┐
│ Sidebar  │  Body (fluid)                             │
│ (240px)  │                                           │
│          │                                           │
│ Menu     │  Main content area                        │
│ items    │                                           │
│          │                                           │
└──────────┴──────────────────────────────────────────┘
// React — sidebar layout
<div style={{ display: 'flex', gap: 'var(--sys-spacing-lg)' }}>
  <aside style={{ width: 240, flexShrink: 0 }}>
    <Card>
      <Menu items={navItems} />
    </Card>
  </aside>
  <main style={{ flex: 1, minWidth: 0 }}>{children}</main>
</div>

Responsive Breakpoints

Apply the breakpoints defined in sys.breakpoint.* (M8 Foundations) to reflow layouts:

BreakpointValueTypical Adaptation
mobile0px (default)Single column, stacked
tablet768pxTwo columns, sidebar appears
desktop1024pxFull layout, sidebar visible
wide1440pxMax-width content container
/* Mobile-first: single column by default */
.page { display: flex; flex-direction: column; }

/* Tablet+: sidebar visible */
@media (min-width: 768px) {
  .page { flex-direction: row; }
  .sidebar { display: block; width: 240px; }
}

React Native has no @media queries — use useWindowDimensions() and compare width against the same breakpoint values from @ghds/tokens.

Live Demo

React @ghds/react

Web Components @ghds/web-components

The system will be updated tonight at 2 AM UTC. Your work will not be affected.
success danger warning info
Delete account

This action cannot be undone. All your data will be permanently removed.

Cancel Delete
// React Native — feedback
import { Alert } from '@ghds/react-native/alert';
import { Button } from '@ghds/react-native/button';
import { Modal } from '@ghds/react-native/modal';
import { Toast } from '@ghds/react-native/toast';

// Transient success
<Toast open={saved} onClose={() => setSaved(false)} variant="success" title="Saved" />;

// Inline warning
<Alert variant="warning" title="Session expiring">Save your work soon.</Alert>;

// Interrupting confirmation
<Modal open={confirming} onClose={() => setConfirming(false)} title="Delete item?">
  <Text>This cannot be undone.</Text>
  <Button label="Delete" variant="danger" onPress={handleDelete} />
</Modal>;

Accessibility

Key Action
Escape Dismiss an open Modal, Toast (close button), or Tooltip.
Tab Move between sidebar items, then into the body.
Enter / Space Activate a sidebar navigation item.