{
  "id": "F05",
  "title": "Live summary",
  "topic": "forms",
  "use": "The column beside the form that recalculates while the user types. It gives three kinds of information: derived values, the consequences of the entry — which approval applies, which entries it makes required — and the state it is calculated against, such as the budget of the cost center. Use it when the user cannot keep track of what their entry sets off.",
  "decisions": [
    "The summary recalculates on every keystroke, not when a field is left: whoever learns only on leaving the field that they crossed a threshold has already written the sentence.",
    "It holds no control — no input, button or choice: it is information, and a field in it would be a second place to fill in the same form.",
    "It says what is still missing and marks nothing as an error: a required field gains its asterisk when it becomes required, and its error appears only on submit; a form that is red on opening teaches that red means nothing.",
    "It sits beside the form on the right, and below it once the form would get less than half the width: it runs along while the eyes work on the left, and above the form it would change before there is anything to calculate.",
    "It holds its place with a gap to the top edge while the form scrolls, and it is short enough to stay fully in view: a summary that stays at the top while the user types at the bottom is none.",
    "Three sections, each answering one question — what does it cost, what follows from it, where do I stand: without the split it is a list of numbers, and the user searches for the one that concerns them.",
    "The approval level is a badge, because it is the value that changes most visibly while typing and a badge shows the jump; the budget is a bar with figures, because the bar shows the ratio and the figures the remainder."
  ],
  "try": [
    {
      "do": "Type a new amount into Hotel per night",
      "then": "the hotel line, the total and the budget bar follow every keystroke, not only when the field is left"
    },
    {
      "do": "Bring the total above €1,000, for example with 650 under Travel in €",
      "then": "Approval changes from Line manager to Line manager and head of division, in a warning badge, and the sentence below says that the request now goes to the head of division as well"
    },
    {
      "do": "Bring the total above €2,500, for example with 2200 under Travel in €",
      "then": "Justification gains its asterisk and the summary names it as still missing, while no field turns red"
    },
    {
      "do": "Switch the cost center to 4712 · Marketing",
      "then": "the budget bar counts against that cost center instead; its budget is almost used up, so the bar fills and the line below says how far the request goes over it"
    },
    {
      "do": "Clear Destination",
      "then": "the summary names the destination as still missing; only Submit travel request checks the form, and then Destination reports the missing entry and takes the focus"
    },
    {
      "do": "Press Save as draft",
      "then": "a toast confirms the draft"
    },
    {
      "do": "Scroll down the form",
      "then": "the summary stays in view beside it"
    },
    {
      "do": "Open the pattern in a new tab and make the window narrower",
      "then": "once the form would get less than half the width, the summary moves below it"
    }
  ],
  "revision": 1,
  "changes": [],
  "reviewed": true,
  "source": "<style>\n  .f05-summary {\n    position: sticky;\n    inset-block-start: var(--ed-space-4);\n  }\n</style>\n\n<div class=\"ed-stack ed-gap-4\">\n  <hgroup class=\"ed-stack ed-gap-0\">\n    <h1 id=\"f05-title\">Request a business trip</h1>\n    <p class=\"ed-caption-m\">Jörg Weiland · Sales · Cost center 4700</p>\n  </hgroup>\n\n  <ed-toolbar label=\"Travel request\">\n    <ed-button id=\"f05-submit\"><ed-icon slot=\"start\" name=\"send\"></ed-icon>Submit travel request</ed-button>\n    <ed-button id=\"f05-draft\" appearance=\"plain\"><ed-icon slot=\"start\" name=\"save\"></ed-icon>Save as draft</ed-button>\n  </ed-toolbar>\n\n  <div class=\"ed-flank:end ed-align-items-start ed-gap-6\" style=\"--ed-flank-size: 19rem\">\n    <div>\n      <form id=\"f05-form\" class=\"ed-stack ed-gap-8\" aria-labelledby=\"f05-title\" style=\"max-inline-size: 64rem\">\n        <section class=\"ed-stack ed-gap-4\">\n          <h2>Trip</h2>\n          <div class=\"ed-grid\">\n            <ed-input id=\"f05-destination\" class=\"ed-span-sm-6\" name=\"destination\" label=\"Destination\" value=\"Hamburg\" required></ed-input>\n            <ed-select class=\"ed-span-sm-6\" name=\"occasion\" label=\"Occasion\" value=\"customer\">\n              <ed-option value=\"customer\">Customer meeting</ed-option>\n              <ed-option value=\"fair\">Trade fair</ed-option>\n              <ed-option value=\"training\">Training course</ed-option>\n              <ed-option value=\"group\">Group meeting</ed-option>\n            </ed-select>\n          </div>\n          <div class=\"ed-grid\">\n            <ed-date-picker id=\"f05-from\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"from\" label=\"From\" value=\"2026-10-05\"></ed-date-picker>\n            <ed-date-picker id=\"f05-to\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"to\" label=\"To\" value=\"2026-10-07\"></ed-date-picker>\n          </div>\n          <div class=\"ed-grid\">\n            <ed-select id=\"f05-cost-center\" class=\"ed-span-sm-6\" name=\"costCenter\" label=\"Cost center\" value=\"4700\">\n              <ed-option value=\"4700\">4700 · Sales back office</ed-option>\n              <ed-option value=\"4711\">4711 · Field sales</ed-option>\n              <ed-option value=\"4712\">4712 · Marketing</ed-option>\n            </ed-select>\n            <ed-select class=\"ed-span-sm-6\" name=\"transport\" label=\"Means of transport\" value=\"train\">\n              <ed-option value=\"train\">Train</ed-option>\n              <ed-option value=\"plane\">Plane</ed-option>\n              <ed-option value=\"company-car\">Company car</ed-option>\n              <ed-option value=\"private-car\">Private car</ed-option>\n            </ed-select>\n          </div>\n        </section>\n\n        <section class=\"ed-stack ed-gap-4\">\n          <div class=\"ed-stack ed-gap-1\">\n            <h2>Estimated costs</h2>\n            <p class=\"ed-caption-m\">Estimates are enough. The trip is settled against receipts afterwards.</p>\n          </div>\n          <div class=\"ed-grid\">\n            <ed-number-input id=\"f05-travel\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"travel\" label=\"Travel in €\" value=\"180\" min=\"0\" step=\"10\" hint=\"€0 or more, in steps of €10\"></ed-number-input>\n            <ed-number-input id=\"f05-nights\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"nights\" label=\"Nights\" value=\"2\" min=\"0\" max=\"30\" hint=\"0 to 30 nights\"></ed-number-input>\n            <ed-number-input id=\"f05-hotel\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"hotel\" label=\"Hotel per night in €\" value=\"140\" min=\"0\" step=\"10\" hint=\"€0 or more, in steps of €10\"></ed-number-input>\n            <ed-number-input id=\"f05-other\" class=\"ed-span-sm-6 ed-span-md-3\" name=\"other\" label=\"Other in €\" value=\"0\" min=\"0\" step=\"10\" hint=\"€0 or more, in steps of €10\"></ed-number-input>\n          </div>\n          <div class=\"ed-grid\">\n            <ed-textarea id=\"f05-justification\" class=\"ed-span-12\" name=\"justification\" label=\"Justification\" rows=\"2\" hint=\"Required from €2,500 in total.\"></ed-textarea>\n          </div>\n        </section>\n      </form>\n    </div>\n\n    <aside class=\"f05-summary\" aria-labelledby=\"f05-summary-title\">\n      <ed-card>\n        <h2 slot=\"header\" id=\"f05-summary-title\">Summary</h2>\n        <div class=\"ed-stack ed-gap-6\">\n          <div class=\"ed-stack ed-gap-2\">\n            <h3 class=\"ed-caption-s\">Costs</h3>\n            <dl class=\"ed-stack ed-gap-1 ed-text-tabular\">\n              <div class=\"ed-split\"><dt>Travel</dt><dd id=\"f05-travel-cost\">€180.00</dd></div>\n              <div class=\"ed-split\"><dt id=\"f05-hotel-label\">Hotel, 2 nights</dt><dd id=\"f05-hotel-cost\">€280.00</dd></div>\n              <div class=\"ed-split\"><dt id=\"f05-per-diem-label\">Per diem, 3 days</dt><dd id=\"f05-per-diem\">€84.00</dd></div>\n              <div id=\"f05-other-row\" class=\"ed-split\" hidden><dt>Other</dt><dd id=\"f05-other-cost\">€0.00</dd></div>\n            </dl>\n            <ed-divider style=\"--ed-divider-spacing: var(--ed-space-1)\"></ed-divider>\n            <dl class=\"ed-text-tabular\">\n              <div class=\"ed-split\"><dt><strong>Total</strong></dt><dd><strong id=\"f05-total\">€544.00</strong></dd></div>\n            </dl>\n          </div>\n\n          <div class=\"ed-stack ed-gap-2\">\n            <h3 class=\"ed-caption-s\">Consequences</h3>\n            <div class=\"ed-stack ed-gap-2\" role=\"status\">\n              <div class=\"ed-split\">\n                <span>Approval</span>\n                <ed-badge id=\"f05-approval\" variant=\"info\">Line manager</ed-badge>\n              </div>\n              <p id=\"f05-consequence\">Up to €1,000.00 the line manager decides.</p>\n            </div>\n          </div>\n\n          <div class=\"ed-stack ed-gap-2\">\n            <h3 class=\"ed-caption-s\">Budget of the cost center</h3>\n            <ed-progress-bar id=\"f05-budget-bar\" value=\"79\" aria-label=\"Budget of the cost center used\"></ed-progress-bar>\n            <div class=\"ed-split ed-text-tabular\">\n              <span id=\"f05-budget-used\">€18,944.00 of €24,000.00</span>\n              <span id=\"f05-remaining\">€5,056.00 free</span>\n              <ed-badge id=\"f05-exceeded\" variant=\"danger\" hidden></ed-badge>\n            </div>\n          </div>\n        </div>\n      </ed-card>\n    </aside>\n  </div>\n</div>\n\n<ed-toast-stack id=\"f05-toasts\"></ed-toast-stack>\n\n<!-- Demo only, not part of the pattern. The product supplies budgetOf(costCenter): the state a cost center is calculated against, as { budget, committed }, answered at once on every keystroke, so it comes from data loaded from accounting beforehand; and its travel rules: perDiemRate, secondApprovalAbove and justificationAbove, in euros. -->\n\n<script type=\"module\">\n  await Promise.all(\n    ['ed-input', 'ed-number-input', 'ed-select', 'ed-date-picker', 'ed-textarea'].map(tag =>\n      customElements.whenDefined(tag)\n    )\n  );\n\n  const euro = new Intl.NumberFormat('en', { style: 'currency', currency: 'EUR' });\n  const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;\n  const byId = id => document.getElementById(`f05-${id}`);\n  const amount = id => Number(byId(id).value) || 0;\n  // Writing only what changed keeps the consequences region from announcing on every keystroke: it\n  // changes at a threshold. The budget line changes with every digit and is no status region.\n  const put = (id, text) => {\n    const element = byId(id);\n    if (element.textContent !== text) element.textContent = text;\n  };\n\n  const form = byId('form');\n  const justification = byId('justification');\n\n  // Both days count: a trip from Monday to Wednesday is three days of per diem.\n  const travelDays = () => {\n    const from = byId('from').value;\n    const to = byId('to').value;\n    return from && to ? Math.max(0, (Date.parse(to) - Date.parse(from)) / 864e5 + 1) : 0;\n  };\n\n  function render() {\n    const nights = amount('nights');\n    const days = travelDays();\n    const hotel = nights * amount('hotel');\n    const perDiem = days * perDiemRate;\n    const total = amount('travel') + hotel + perDiem + amount('other');\n\n    put('total', euro.format(total));\n    put('travel-cost', euro.format(amount('travel')));\n    put('hotel-label', `Hotel, ${count(nights, 'night')}`);\n    put('hotel-cost', euro.format(hotel));\n    put('per-diem-label', `Per diem, ${count(days, 'day')}`);\n    put('per-diem', euro.format(perDiem));\n    put('other-cost', euro.format(amount('other')));\n    byId('other-row').hidden = amount('other') === 0;\n\n    // Required is enforced on submit: the field gains its asterisk now, and its error only then. What\n    // is still missing is named here instead of marked on the field.\n    const needsJustification = total > justificationAbove;\n    justification.required = needsJustification;\n    const second = total > secondApprovalAbove;\n    const approval = byId('approval');\n    approval.variant = second ? 'warning' : 'info';\n    put('approval', second ? 'Line manager and head of division' : 'Line manager');\n    const missing = [\n      ...(needsJustification && !justification.value.trim() ? ['a justification'] : []),\n      ...(!byId('destination').value.trim() ? ['the destination'] : [])\n    ];\n    put(\n      'consequence',\n      missing.length\n        ? `Still missing for this request: ${missing.join(' and ')}.`\n        : second\n          ? `Above ${euro.format(secondApprovalAbove)} the request also goes to the head of division.`\n          : `Up to ${euro.format(secondApprovalAbove)} the line manager decides.`\n    );\n\n    const center = byId('cost-center').value;\n    const { budget, committed } = budgetOf(center);\n    const used = committed + total;\n    const remaining = budget - used;\n    byId('budget-bar').value = Math.min(100, Math.round((used / budget) * 100));\n    put('budget-used', `${euro.format(used)} of ${euro.format(budget)}`);\n    put('remaining', `${euro.format(remaining)} free`);\n    put('exceeded', `${euro.format(-remaining)} over budget`);\n    byId('remaining').hidden = remaining < 0;\n    byId('exceeded').hidden = remaining >= 0;\n  }\n\n  // On every keystroke, not when a field is left: input covers typing, change the controls that\n  // report only when a choice is committed.\n  form.addEventListener('input', render);\n  form.addEventListener('change', render);\n\n  byId('submit').addEventListener('click', () => form.requestSubmit());\n  form.addEventListener('submit', event => {\n    event.preventDefault();\n    byId('toasts').create({\n      variant: 'success',\n      title: 'Travel request submitted',\n      description:\n        byId('approval').variant === 'warning'\n          ? 'Approval by the line manager and the head of division.'\n          : 'Approval by the line manager.'\n    });\n  });\n\n  // Whether a draft may keep data that is not yet checked is a legal question still open, so the\n  // button confirms and promises nothing about what is kept.\n  byId('draft').addEventListener('click', () => {\n    byId('toasts').create({ variant: 'success', title: 'Draft saved' });\n  });\n\n  render();\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/forms/f05-live-summary/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/forms/"
}
