# Error summary

A box at the top of a form after a failed send, listing every mistake as a link that moves focus to its field.

Block: `smbk-error-summary`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `md`, `sm` | `md` | Small sits closer to its edges with a smaller heading, for a dialog or a narrow form. |

Parts: `smbk-error-summary__icon`, `smbk-error-summary__content`, `smbk-error-summary__text`, `smbk-error-summary__heading`, `smbk-error-summary__intro`, `smbk-error-summary__list`, `smbk-error-summary__item`, `smbk-error-summary__link`

## Markup

```html
<div
  class="smbk-error-summary smbk-error-summary--size-md"
  tabindex="-1"
  role="group"
  aria-labelledby="error-summary-1-heading"
>
  <span class="smbk-error-summary__icon">
    <i class="ph ph-x-circle smbk-icon smbk-icon--size-md" aria-hidden="true"></i>
  </span>
  <div class="smbk-error-summary__content">
    <div class="smbk-error-summary__text">
      <h2 id="error-summary-1-heading" class="smbk-error-summary__heading">There is a problem</h2>
      <p class="smbk-error-summary__intro">Fix these so we can send your details:</p>
    </div>
    <ul class="smbk-error-summary__list">
      <li class="smbk-error-summary__item"><a class="smbk-error-summary__link" href="#preview-email">Enter your email address</a></li>
      <li class="smbk-error-summary__item"><a class="smbk-error-summary__link" href="#preview-speed">Choose a delivery speed</a></li>
      <li class="smbk-error-summary__item"><a class="smbk-error-summary__link" href="#preview-date">Enter a date in the future</a></li>
    </ul>
  </div>
</div>
```

## React

```tsx
import { ErrorSummary } from './design-system/components/error-summary/error-summary.js';

<ErrorSummary size="md" errors={[{ id: "preview-email", message: "Enter your email address" }, { id: "preview-speed", message: "Choose a delivery speed" }, { id: "preview-date", message: "Enter a date in the future" }]} intro="Fix these so we can send your details:" />
```

## Stylesheet

```css
/* smbk-error-summary — Error summary. Figma "Error summary" 7110:412 (Size × State) and "Error summary item"
 * 7110:350 (Type). A box on the danger family's subtle surface, inside a hairline of its edge colour, at the small
 * container corners: the danger icon, then the heading over an optional intro line, then the mistakes, each a link
 * in the danger text colour, underlined, that moves focus to its field. It is focused by the page after a failed
 * send, and draws the focus ring round itself whenever it has focus, from a click as from the keyboard. */

.smbk-error-summary {
  /* Always the danger family (the tone recipe). */
  --smbk-tone-surface: var(--smbk-color-feedback-danger-surface-subtle);
  --smbk-tone-border: var(--smbk-color-feedback-danger-border);
  --smbk-tone-text: var(--smbk-color-feedback-danger-text-on-subtle);
  --smbk-tone-icon: var(--smbk-color-feedback-danger-icon);
  box-sizing: border-box;
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-lg);
  padding: var(--smbk-space-inset-xl);
  background: var(--smbk-tone-surface);
  border: var(--smbk-border-width-hairline) solid var(--smbk-tone-border);
  border-radius: var(--smbk-radius-container-sm);
  color: var(--smbk-tone-text);
}

/* The page moves focus here; the ring shows for that focus too, not only for the keyboard's. */
.smbk-error-summary:focus {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

/* The icon sits in a box one heading line tall, so it is centred on the heading's first line at either size. */
.smbk-error-summary__icon {
  flex: none;
  display: flex;
  align-items: center;
  height: 1lh;
  font-family: var(--smbk-typography-subtitle-font-family);
  font-size: var(--smbk-typography-subtitle-font-size);
  line-height: var(--smbk-typography-subtitle-line-height);
  color: var(--smbk-tone-icon);
}

.smbk-error-summary__icon .smbk-icon {
  font-size: var(--smbk-size-icon-md);
  color: inherit;
}

.smbk-error-summary__content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-md);
}

.smbk-error-summary__text {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-text-label);
}

.smbk-error-summary__heading {
  margin: 0;
  color: var(--smbk-tone-text);
  font-family: var(--smbk-typography-subtitle-font-family);
  font-size: var(--smbk-typography-subtitle-font-size);
  font-weight: var(--smbk-typography-subtitle-font-weight);
  line-height: var(--smbk-typography-subtitle-line-height);
  letter-spacing: var(--smbk-typography-subtitle-letter-spacing);
}

.smbk-error-summary__intro {
  margin: 0;
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
}

.smbk-error-summary__list {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

.smbk-error-summary__item {
  margin: 0;
  color: var(--smbk-tone-text);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
}

/* The link keeps the danger text colour and its underline at rest, on hover and when pressed: the summary's colour
   is what ties it to the fields' messages. */
.smbk-error-summary__link {
  color: inherit;
  text-decoration: underline;
  border-radius: var(--smbk-radius-control-sm);
  -webkit-tap-highlight-color: transparent;
}

.smbk-error-summary__link:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

/* ---- small: closer to its edges, the heading in Label LG ---- */

.smbk-error-summary--size-sm {
  padding: var(--smbk-space-inset-lg);
}

.smbk-error-summary--size-sm .smbk-error-summary__icon {
  font-family: var(--smbk-typography-label-lg-font-family);
  font-size: var(--smbk-typography-label-lg-font-size);
  line-height: var(--smbk-typography-label-lg-line-height);
}

.smbk-error-summary--size-sm .smbk-error-summary__heading {
  font-family: var(--smbk-typography-label-lg-font-family);
  font-size: var(--smbk-typography-label-lg-font-size);
  font-weight: var(--smbk-typography-label-lg-font-weight);
  line-height: var(--smbk-typography-label-lg-line-height);
  letter-spacing: var(--smbk-typography-label-lg-letter-spacing);
}

@media (forced-colors: active) {
  .smbk-error-summary:focus,
  .smbk-error-summary__link:focus-visible {
    outline-color: Highlight;
  }
}

/* ---- touch: each link takes an invisible press area ----
   On a touch screen an invisible area, centred on each link, takes the press out to the comfortable target in each
   direction it falls short of it, and between two links only halfway into the list's gap, so the links stay one
   press each. The links, their focus ring and their places stay as drawn. */

@media (pointer: coarse) {
  .smbk-error-summary__link {
    position: relative;
  }

  .smbk-error-summary__link::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }

  /* Halfway between two links: half the list's gap and half of what the line keeps round the link's own box. */
  .smbk-error-summary__item:has(.smbk-error-summary__link) + .smbk-error-summary__item .smbk-error-summary__link::after {
    inset-block-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-stack-md) + 1lh - 100%) / -2);
  }

  .smbk-error-summary__item:has(+ .smbk-error-summary__item .smbk-error-summary__link) .smbk-error-summary__link::after {
    inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-stack-md) + 1lh - 100%) / -2);
  }
}
```

## Tokens it reads

```json
{
  "color": {
    "feedback": {
      "danger": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.red.200}}",
          "$description": "Danger messages: pale fill."
        },
        "border": {
          "$type": "color",
          "$value": "{{color.red.500}}",
          "$description": "Danger messages: edge; the edge of a field in error (3:1)."
        },
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.red.700}}",
          "$description": "Danger messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    },
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      }
    }
  },
  "space": {
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      }
    },
    "inset": {
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      }
    },
    "stack": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things stacked one above another, medium."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "radius": {
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    },
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "typography": {
    "subtitle": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "20px",
        "fontWeight": 400,
        "lineHeight": 1.4,
        "letterSpacing": "0px"
      },
      "$description": "Subtitle: a short heading set in body text, such as the error summary’s title. Body LG’s size with closer lines."
    },
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "label-lg": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 500,
        "lineHeight": 1.4,
        "letterSpacing": "0px"
      },
      "$description": "Label LG: labels on controls and short UI text (buttons, chips, tabs, menu headings). Geist Medium, sentence case. Code and keys use Code SM and Code MD."
    }
  },
  "size": {
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  }
}
```

## Purpose

Lists every mistake in a form at its top, after the form was sent with mistakes, so a reader sees at once how many things need fixing and can go straight to each. Each mistake is a link to its field. The message beside each field still says what is wrong there; the summary gathers those messages in one place.

## Anatomy

- The block, a box on the danger color's subtle surface, inside a hairline edge, with small container corners.
- The icon part, the danger icon, on the heading's first line.
- The content part, holding the text and the list.
- The text part: the heading part ("There is a problem") over the optional intro part, a line such as "Fix these so we can send your details:".
- The list part, one item part per mistake, in the order the fields come in the form.
- The link part, inside an item whose mistake belongs to a field: the mistake's words, underlined, in the danger text color. An item for a problem with the whole form holds its words alone.

## When to use

- At the top of a form, after it was sent with one mistake or more.
- For a long form, or one that runs over more than one screen, where a reader cannot see every field's message at once.

## When not to use

- Before the form is sent. Check fields as they are left, if at all, and show the message beside the field.
- For a problem the reader cannot fix, such as a server that is down. Use an [alert](alert.md) or an [empty state](empty-state.md).
- For a single short field, such as a search box. Its own message is enough.

## Behavior

The summary takes focus as it appears, so the page scrolls to it and a screen reader reads its heading. Send the form again with mistakes still in it and the summary takes focus again: give it a `focusKey` that changes with each send, such as a count of sends. A summary that shows only after the page reloads takes focus as the page loads.

Pressing a mistake's link brings the field's label into view and moves focus to the field. For a group of choices, such as radio buttons, focus moves to the first choice marked invalid, or the first choice, and the group's question comes into view. Without script, the link goes to the field's id, as any link to a part of a page does.

While it has focus, the summary draws the focus ring round itself, whether focus came from a press, the keyboard or the page.

## Accessibility

### What it does for you

- The summary is a group named by its heading, and the page moves focus to it, so a screen reader reads "There is a problem" without a live region. It is not a Tab stop: Tab from the summary reaches its first link.
- Each mistake with a field is a link, so it can be reached with Tab and listed with a screen reader's list of links.
- The heading's level follows the page's outline: 2 by default.
- The words carry the meaning; the danger color only backs them up.

### What you must do

- Word each mistake exactly as the message beside its field, and say how to fix it: "Enter a date in the future", never "Invalid date".
- Give each field the id its mistake names. Give a group of choices its id on the group (the field group's fieldset), and its choices are reached through it.
- Set the heading's level with `headingLevel` to follow the page's outline.
- Give it a `focusKey` that changes with each send, so it takes focus again when the form is sent with mistakes still in it.

### How to test

- Send the form with mistakes: focus moves to the summary and a screen reader reads its heading.
- Tab from the summary to the first link, then press Enter: focus moves to the field, with its label in view.
- With a screen reader's list of links, each mistake appears as a link.
- With text at 200% and the page 320 pixels wide, the words and links stay whole.
- In a forced-color mode, the summary's focus ring and the links are still visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus from the summary to its first link, then on through the links. |
| Enter | Follows the focused link, moving focus to its field. |

## Usage rules

### Do

- Show it only after the form was sent with mistakes, and take it away when they are fixed and the form is sent.
- List the mistakes in the order of the fields.
- Use the same words in the summary and beside the field.

### Don't

- Show a summary with no message beside each field.
- Move focus to it while someone is still filling in the form.
- List problems the reader cannot fix.
