DEIN TÄGLICHES UI/UX-COACHING / LEKTION 08

Microinteractions: Wenn das Schließen wichtiger ist als das Öffnen

Beim Öffnen erscheint ein Dialog sanft. Beim Schließen ist er plötzlich weg. Heute untersuchst du, warum das in React passiert – und wie Animation, Fokus und Bedienbarkeit gemeinsam einen verständlichen Zustandswechsel ergeben.

ca. 15 Minuten inklusive Übung

Dein Lernziel

Du trennst den Öffnungswunsch, die Animationsphase und den tatsächlichen Dialogzustand. Deine Exit-Animation kann auslaufen, ohne Escape, Fokusrückkehr oder reduzierte Bewegung zu beeinträchtigen.

01 / DEN ZUSTANDSWECHSEL VERSTEHEN · 3 MINUTEN

Enter, Open, Exit – und erst dann geschlossen.

In Lektion 7 hast du einen Filterdialog gestaltet. Jetzt betrachten wir dessen Übergänge. Ein typischer React-Entwurf rendert das Overlay nur, solange eine boolesche Variable wahr ist:

{isOpen && <Overlay />}

Sobald isOpen auf false wechselt, entfernt React das Overlay aus dem DOM. Eine Exit-Animation hat dann kein sichtbares Element mehr. Ein neuer CSS-Keyframe allein löst dieses Problem nicht.

  1. Enter: Der Dialog ist vorhanden und erscheint. Er gehört bereits zum modalen Interaktionsablauf.
  2. Open: Der Übergang ist abgeschlossen. Menschen können die Inhalte bedienen.
  3. Exit: Der Schließwunsch liegt vor. Der Dialog verschwindet visuell, bleibt aber bis zum Abschluss modal.
  4. Closed: Erst jetzt wird der native Dialog mit close() geschlossen und der Hintergrund wieder freigegeben.

Die Musterlösung lässt das native <dialog>-Element im DOM. Es ist im geschlossenen Zustand nicht sichtbar. Du musst es nicht unmounten: Entscheidend ist, den tatsächlichen Schließzeitpunkt von der Exit-Phase zu trennen.

02 / MOTION BEGRÜNDEN

Wie viel Bewegung braucht diese Aufgabe?

Für einen kleinen Einstellungsdialog beginnen wir im Labor mit 220 ms beim Öffnen, 160 ms beim Schließen und 16 px Bewegung. Das sind experimentelle Ausgangswerte für diese Übung, keine verbindlichen UX-Standards.

Eine kürzere Exit-Animation ist eine plausible Hypothese: Wer den Dialog verlassen möchte, soll nicht unnötig warten. Bei häufig wiederholten Aufgaben kann auch eine sehr dezente oder ganz entfallende Animation sinnvoll sein. Beurteile Timing im tatsächlichen Ablauf, nicht nur als isolierten Effekt.

Im Labor kannst du drei Varianten vergleichen. Die Vorschau verändert nur ein fiktives Leseelement dieser Lektion. Sie speichert keine Einstellung im Browser oder auf einem Server.

03 / DEINE CHALLENGE · 8 MINUTEN

Ein Dialog, der auch richtig schließt.

Baue in React und CSS ein kleines Einstellungsdialogfeld. Nutze das native <dialog>, damit du die modalen Grundlagen nicht vollständig selbst nachbauen musst.

  1. Öffnen: Eine leichte Bewegung nach oben und Einblendung erklären das Erscheinen.
  2. Schließen: Eine kürzere Exit-Animation läuft aus, bevor du close() aufrufst.
  3. Bedienen: Ein sichtbarer Schließen-Button und Escape lösen denselben Schließweg aus. Der Fokus kehrt zum Auslöser zurück.
  4. Bewegung reduzieren: Bei prefers-reduced-motion: reduce entfällt die räumliche Bewegung. Du kannst die Animation vollständig überspringen, wenn der Zustand trotzdem zuverlässig weitergeschaltet wird.
  5. Robust bleiben: Wiederholtes Schließen, ein Schließwunsch während Enter und ein ausbleibendes Animationsende dürfen den Dialog nicht blockieren.

Teste außerdem eine schmale Ansicht, große Schrift und die Tab-Reihenfolge. Begründe dein Timing und entscheide ausdrücklich, was bei einem erneuten Öffnungswunsch während Exit passieren soll.

04 / MUSTERLÖSUNG MIT MOTION-LABOR

Der Übergang ist Teil der Interaktion.

Interaktives Motion-Labor öffnen
Öffnen
220 ms
Schließen
160 ms
Bewegung
16 px

Die Systemeinstellung für reduzierte Bewegung wird zusätzlich berücksichtigt. Ohne Animation werden die Phasen sofort abgeschlossen. Im Ereignis-Test greift stattdessen nach der Animation ein Ersatzzeitlimit von zusätzlich 100 ms.

VORSCHAU · LOKALE EINSTELLUNGRaum zum Lesen.

Großzügige Abstände sind aktiv.

Phase: Geschlossen

Letzte Phasen: closed

Leseansicht einstellen

Diese Einstellung verändert nur die Beispielvorschau dieser Lektion und bleibt bis zum Neuladen erhalten.

Geschlossen

Auch Escape schließt den Dialog. Die Hintergrundseite wird erst nach dem Ende der Exit-Phase wieder bedienbar.

So kannst du die Lösung prüfen

  1. Vergleiche „Schnell“, „Ausgewogen“ und „Langsam“. Öffne den Dialog mehrmals und schließe ihn über Button und Escape.
  2. Aktiviere im Dialog kompakte Abstände. Die lokale Vorschau behält die Auswahl beim Schließen und erneuten Öffnen.
  3. Aktiviere „Reduzierte Bewegung ausprobieren“. Öffnen und Schließen sollen jetzt ohne räumliche Bewegung und ohne Wartezeit auf eine Animation funktionieren.
  4. Simuliere ein ausbleibendes Animationsende. Die Ersatzfrist darf den Dialog trotzdem freigeben. Sie ist ein Notweg und verlängert den Ablauf bewusst um 100 ms.
  5. Prüfe die letzten Phasen. Im normalen Ablauf folgt auf entering ein open und auf exiting ein closed. Ein früher Schließwunsch darf Enter abkürzen.

Warum diese Entscheidungen?

  • Explizite Phasen: Der Dialog wird sofort modal geöffnet, aber erst nach Exit tatsächlich geschlossen. Ein zusätzlicher Ref hält den aktuellen Zustand auch bei rasch aufeinanderfolgenden Ereignissen fest.
  • Ein gemeinsamer Schließweg: Escape verhindert zunächst das sofortige native Schließen und fordert stattdessen Exit an. Mehrere Schließwünsche starten keine zusätzlichen Timer.
  • Nur das passende Animationsende zählt: Name und Ziel des Ereignisses müssen zur aktuellen Dialoganimation passen. Ereignisse von Kind- und Pseudoelementen oder einer vorherigen Phase lösen keinen verfrühten Abschluss aus.
  • Ein fehlendes Ereignis blockiert nicht: animationcancel und ein Ersatzzeitlimit sichern den Abschluss ab. Timer und Listener werden beim Phasenwechsel und Unmounting aufgeräumt; veraltete Rückrufe werden unwirksam.
  • Reduzierte Bewegung ohne Ereignis-Trick: CSS entfernt Bewegung und Animation. JavaScript überspringt den animierten Übergang. Eine künstliche 1-ms-Animation ist nicht nötig. Die Testoption kann die Systemeinstellung nicht aufheben.
  • Ein bewusster Nachteil: Während Exit bleibt die Hintergrundseite gesperrt. Im normalen Beispiel betrifft das 100–300 ms. Bei einer häufigen Aufgabe solltest du prüfen, ob diese Verzögerung noch hilft oder entfallen sollte.

Das Timing und die Gestaltung sind eigene, ungetestete Vorschläge. Die Einstellung wird sofort auf die Beispielvorschau angewendet; „Schließen“ ist hier kein Speichern- oder Abbrechen-Dialog.

React-Code, CSS und Animationssteuerung ansehen

Dies ist der tatsächliche Code des Motion-Labors. Die Hilfsfunktion ist auf die einzelne, von der Komponente gesteuerte Dialoganimation ausgelegt; beim Übernehmen sind die Importpfade anzupassen.

coaching-animated-dialog.tsx

"use client";

import { useEffect, useId, useRef, useState, type CSSProperties } from "react";
import { animationBudget, motionPresets, waitForDialogAnimation, type DialogPhase } from "@/lib/coaching-dialog-motion";
import "./coaching-animated-dialog.css";

const phaseLabels: Record<DialogPhase, string> = { closed: "Geschlossen", entering: "Enter · erscheint", open: "Open · bedienbar", exiting: "Exit · schließt" };

export function CoachingAnimatedDialog() {
  const [preset, setPreset] = useState<keyof typeof motionPresets>("balanced");
  const [phase, setPhase] = useState<DialogPhase>("closed");
  const currentPhase = useRef<DialogPhase>("closed");
  const [history, setHistory] = useState<DialogPhase[]>(["closed"]);
  const [simulateReduced, setSimulateReduced] = useState(false);
  const [systemReduced, setSystemReduced] = useState(false);
  const [skipEvents, setSkipEvents] = useState(false);
  const [compact, setCompact] = useState(false);
  const dialog = useRef<HTMLDialogElement>(null);
  const heading = useRef<HTMLHeadingElement>(null);
  const id = useId();
  const timing = motionPresets[preset];
  const reduced = simulateReduced || systemReduced;

  function moveTo(next: DialogPhase) {
    if (currentPhase.current === next) return;
    currentPhase.current = next;
    setPhase(next);
    setHistory(previous => [...previous.slice(-5), next]);
  }

  useEffect(() => {
    const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setSystemReduced(preference.matches);
    update();
    preference.addEventListener("change", update);
    return () => preference.removeEventListener("change", update);
  }, []);

  useEffect(() => {
    const element = dialog.current;
    if (!element || (phase !== "entering" && phase !== "exiting")) return;
    const complete = () => {
      if (currentPhase.current !== phase) return;
      if (phase === "exiting") {
        element.close();
        moveTo("closed");
      } else moveTo("open");
    };
    const expected = phase === "entering" ? "coaching-dialog-enter" : "coaching-dialog-exit";
    const style = window.getComputedStyle(element);
    const duration = animationBudget(style.animationDuration, style.animationDelay);
    if (reduced || style.animationName !== expected || duration === 0) {
      complete();
      return;
    }
    return waitForDialogAnimation(element, expected, duration, complete, !skipEvents);
  }, [phase, reduced, skipEvents]);

  function openDialog() {
    if (!dialog.current || dialog.current.open) return;
    dialog.current.showModal();
    moveTo("entering");
    heading.current?.focus();
  }

  function requestClose() {
    if (currentPhase.current === "closed" || currentPhase.current === "exiting") return;
    moveTo("exiting");
  }

  const variables = { "--motion-enter": `${timing.enter}ms`, "--motion-exit": `${timing.exit}ms`, "--motion-distance": `${timing.distance}px` } as CSSProperties;
  return <div className="motion-lab">
    <div className="motion-presets" role="group" aria-label="Motion-Timing auswählen">{Object.entries(motionPresets).map(([key, value]) => <button key={key} type="button" aria-pressed={preset === key} onClick={() => setPreset(key as keyof typeof motionPresets)}>{value.label}</button>)}</div>
    <dl className="motion-metrics"><div><dt>Öffnen</dt><dd>{reduced ? 0 : timing.enter} <span>ms</span></dd></div><div><dt>Schließen</dt><dd>{reduced ? 0 : timing.exit} <span>ms</span></dd></div><div><dt>Bewegung</dt><dd>{reduced ? 0 : timing.distance} <span>px</span></dd></div></dl>
    <div className="motion-options"><label><input type="checkbox" checked={simulateReduced} onChange={event => setSimulateReduced(event.target.checked)} /> Reduzierte Bewegung ausprobieren</label><label><input type="checkbox" checked={skipEvents} onChange={event => setSkipEvents(event.target.checked)} /> Ausbleibendes Animationsende simulieren</label></div>
    <p className="motion-note">{systemReduced ? "Deine Systemeinstellung reduziert Bewegung. Sie bleibt auch ohne die Testoption aktiv." : "Die Systemeinstellung für reduzierte Bewegung wird zusätzlich berücksichtigt."} Ohne Animation werden die Phasen sofort abgeschlossen. Im Ereignis-Test greift stattdessen nach der Animation ein Ersatzzeitlimit von zusätzlich 100 ms.</p>
    <div className={`motion-reading-preview${compact ? " is-compact" : ""}`}><span>VORSCHAU · LOKALE EINSTELLUNG</span><strong>Raum zum Lesen.</strong><p>{compact ? "Kompakte Abstände sind aktiv." : "Großzügige Abstände sind aktiv."}</p></div>
    <button type="button" className="motion-open" onClick={openDialog} aria-haspopup="dialog" aria-controls={`${id}-dialog`}>Einstellungen öffnen</button>
    <p className="motion-current">Phase: <strong>{phaseLabels[phase]}</strong></p><p className="motion-history">Letzte Phasen: {history.join(" → ")}</p>
    <dialog id={`${id}-dialog`} ref={dialog} className="coaching-motion-dialog" data-phase={phase} data-reduced={reduced} style={variables} aria-labelledby={`${id}-title`} onCancel={event => { event.preventDefault(); requestClose(); }} onClose={() => { if (!dialog.current?.open) moveTo("closed"); }}>
      <div className="motion-dialog-content"><h3 id={`${id}-title`} ref={heading} tabIndex={-1}>Leseansicht einstellen</h3><p>Diese Einstellung verändert nur die Beispielvorschau dieser Lektion und bleibt bis zum Neuladen erhalten.</p><label><input type="checkbox" checked={compact} onChange={event => setCompact(event.target.checked)} /> Kompakte Abstände</label><p className="motion-dialog-phase">{phaseLabels[phase]}</p><button type="button" onClick={requestClose}>Schließen</button><p className="motion-note">Auch Escape schließt den Dialog. Die Hintergrundseite wird erst nach dem Ende der Exit-Phase wieder bedienbar.</p></div>
    </dialog>
  </div>;
}

coaching-animated-dialog.css

.motion-lab{max-width:860px;padding:24px;border:1px solid #cbd5e5;border-radius:12px;background:#f0f4fb;margin-top:24px;min-width:0}
.motion-presets{display:flex;flex-wrap:wrap;gap:8px}
.motion-presets button,.motion-open,.motion-dialog-content button{min-height:44px;padding:10px 16px;border:1px solid #748199;border-radius:7px;font-size:13px;line-height:1.6;background:white;color:#23334f}
.motion-presets button[aria-pressed=true],button.motion-open,.motion-dialog-content button{background:#2558ed;color:white;border-color:#2558ed}
.motion-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;padding:24px 0;border-bottom:1px solid #cbd5e5;margin-bottom:16px}
.motion-metrics dt{font-size:12px;line-height:1.8;color:#536174}
.motion-metrics dd{font-size:32px;font-weight:500;line-height:1.5;font-variant-numeric:tabular-nums}
.motion-metrics dd span{font-size:13px;color:#536174;font-weight:400}
.motion-options label,.motion-dialog-content label{display:flex;align-items:flex-start;gap:12px;font-size:14px;line-height:1.7;min-height:44px;padding:10px 0;cursor:pointer}
.motion-options input,.motion-dialog-content input{width:18px;height:18px;flex-shrink:0;accent-color:#2558ed;margin-top:3px}
.lesson-content p.motion-note,.motion-dialog-content p.motion-note{font-size:12px;line-height:1.8;color:#536174;margin-top:12px}
.motion-reading-preview{padding:24px;border:1px solid #cbd5e5;border-radius:8px;background:white;margin:24px 0}
.motion-reading-preview>span{font:9px/1.8 monospace;letter-spacing:.07em;color:#536174}
.motion-reading-preview>strong{display:block;font-size:24px;line-height:1.5;font-weight:500;margin-top:16px}
.lesson-content .motion-reading-preview>p{font-size:14px;line-height:1.8;margin-top:16px;color:#536174}
.motion-reading-preview.is-compact{padding:16px}.motion-reading-preview.is-compact>strong,.lesson-content .motion-reading-preview.is-compact>p{margin-top:8px}
.lesson-content .motion-current{font-size:14px;line-height:1.7;margin-top:20px;color:#253956}
.lesson-content .motion-history{font:11px/1.9 monospace;margin-top:8px;color:#536174;overflow-wrap:anywhere}
.coaching-motion-dialog{position:fixed;inset:0;margin:auto;width:420px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 48px);padding:0;border:1px solid #aebdd6;border-radius:16px;background:white;color:#172131;box-shadow:0 20px 60px #101a3040;overflow:auto;overscroll-behavior:contain}
.coaching-motion-dialog::backdrop{background:#101a3099}
.motion-dialog-content{padding:24px}
.motion-dialog-content h3{font:500 26px/1.4 system-ui,sans-serif;margin:0;scroll-margin-top:20px}
.motion-dialog-content p{font-size:14px;line-height:1.8;color:#536174;margin-top:16px}
.motion-dialog-content label{margin-top:16px}
.motion-dialog-content button{margin-top:16px}
.motion-dialog-content p.motion-dialog-phase{font-size:12px;color:#304b80}
.coaching-motion-dialog[data-phase=entering]{animation:coaching-dialog-enter var(--motion-enter) ease-out both}
.coaching-motion-dialog[data-phase=exiting]{animation:coaching-dialog-exit var(--motion-exit) ease-in both}
.coaching-motion-dialog[data-phase=entering]::backdrop{animation:coaching-backdrop-enter var(--motion-enter) ease-out both}
.coaching-motion-dialog[data-phase=exiting]::backdrop{animation:coaching-backdrop-exit var(--motion-exit) ease-in both}
@keyframes coaching-dialog-enter{from{opacity:0;transform:translateY(var(--motion-distance)) scale(.98)}to{opacity:1;transform:none}}
@keyframes coaching-dialog-exit{from{opacity:1;transform:none}to{opacity:0;transform:translateY(calc(var(--motion-distance) / 2)) scale(.98)}}
@keyframes coaching-backdrop-enter{from{background:#101a3000}to{background:#101a3099}}
@keyframes coaching-backdrop-exit{from{background:#101a3099}to{background:#101a3000}}
.coaching-motion-dialog[data-reduced=true],.coaching-motion-dialog[data-reduced=true]::backdrop{animation:none;transform:none}
@media(prefers-reduced-motion:reduce){.coaching-motion-dialog,.coaching-motion-dialog::backdrop{animation:none!important;transform:none!important}}
.motion-lab :is(button,input):focus-visible,.coaching-motion-dialog :is(button,input,h3):focus-visible{outline:3px solid #2558ed;outline-offset:3px}
@media(max-width:640px){.motion-lab,.motion-dialog-content{padding:18px}.motion-metrics{gap:8px}.motion-metrics dd{font-size:26px}.motion-metrics dd span{display:block}.motion-reading-preview{padding:18px}}

coaching-dialog-motion.ts

export type DialogPhase = "closed" | "entering" | "open" | "exiting";
export const motionPresets = {
  fast: { label: "Schnell", enter: 140, exit: 100, distance: 8 },
  balanced: { label: "Ausgewogen", enter: 220, exit: 160, distance: 16 },
  slow: { label: "Langsam", enter: 420, exit: 300, distance: 24 },
} as const;

export function animationBudget(duration: string, delay: string): number {
  const milliseconds = (time: string) => Math.max(0, parseFloat(time) * (time.trim().endsWith("ms") ? 1 : 1000) || 0);
  return milliseconds(duration) + milliseconds(delay);
}

type Scheduler = (callback: () => void, delay: number) => () => void;
const defaultSchedule: Scheduler = (callback, delay) => {
  const timer = setTimeout(callback, delay);
  return () => clearTimeout(timer);
};

// Events from children, pseudo-elements and previous phases must not finish this phase.
// The returned cleanup invalidates stale callbacks as well as removing listeners/timers.
export function waitForDialogAnimation(
  element: HTMLElement,
  name: string,
  duration: number,
  complete: () => void,
  listen = true,
  schedule: Scheduler = defaultSchedule,
): () => void {
  let settled = false;
  let cancelTimer = () => {};
  function cleanup() {
    settled = true;
    cancelTimer();
    element.removeEventListener("animationend", onAnimation);
    element.removeEventListener("animationcancel", onAnimation);
  }
  function finish() {
    if (settled) return;
    cleanup();
    complete();
  }
  function onAnimation(event: Event) {
    const animation = event as AnimationEvent;
    if (event.target === element && animation.animationName === name && !animation.pseudoElement) finish();
  }
  if (listen) {
    element.addEventListener("animationend", onAnimation);
    element.addEventListener("animationcancel", onAnimation);
  }
  cancelTimer = schedule(finish, duration + 100);
  return cleanup;
}

05 / BEDIENBARKEIT VOR EFFEKT

Fokus und Schließen müssen verlässlich bleiben.

Der Fokus geht beim Öffnen zur Dialogüberschrift. Die Hintergrundseite ist während der Modalität nicht bedienbar; die Tab-Navigation erreicht die Dialogelemente. Nach dem tatsächlichen Schließen kehrt der Fokus zum Auslöser zurück. Diese Regeln gelten unabhängig davon, wie schön die Animation ist. W3C: Modal Dialog Pattern.

prefers-reduced-motion erkennt eine entsprechende Präferenz des Systems. Weniger Bewegung bedeutet nicht, dass eine Aktion unverständlich werden darf: Zustand, Beschriftung und Fokus liefern weiterhin Rückmeldung. In dieser Übung verzichten wir unter dieser Präferenz auf die gesamte Animation. MDN: prefers-reduced-motion.

Auf animationend allein kannst du dich nicht verlassen. Wird eine Animation vorzeitig abgebrochen, bleibt dieses Ereignis aus. Deshalb braucht der Lebenszyklus einen zusätzlichen Abschlussweg. MDN: animationend.

06 / REFLEXION · 3 MINUTEN

Wann hilft der Übergang – und wann hält er auf?

Ein Overlay wird mehrmals pro Minute geöffnet.

Würdest du Exit genauso lang wie Enter gestalten, kürzer und zurückhaltender oder ganz darauf verzichten? Begründe deine Entscheidung anhand der Aufgabe.

Mögliche Antwort zur Nutzungshäufigkeit

Kürzer und zurückhaltender wäre mein erster Versuch. Bei sehr häufigem Wechsel kann auch ein sofortiges Schließen die bessere Variante sein. Ich würde beobachten, ob der Übergang Orientierung gibt oder Menschen auf die nächste Aktion warten lässt. Eine pauschal gleiche Dauer für Enter und Exit ist durch die Aufgabe noch nicht begründet.

Vergleiche beide Varianten im vollständigen Ablauf. Achte auf Wartezeiten, unbeabsichtigte Eingaben und darauf, ob Menschen den Wechsel nachvollziehen. Die langsamste Variante im Labor dient dem Vergleich und ist keine Empfehlung für den Alltag.

Während Exit wird erneut der Dialog benötigt.

Was soll mit dem neuen Wunsch passieren? Unterscheide einen normalen Klick im Hintergrund von einer programmatisch ausgelösten Folgeaktion.

Mögliche Antwort zu schnellen Folgeaktionen

Ich würde die Regel ausdrücklich festlegen. In dieser Musterlösung bleibt der Hintergrund bis close() gesperrt. Ein weiterer Schließwunsch wird ignoriert; erneutes Öffnen ist danach möglich. Es wird kein Öffnungswunsch stillschweigend vorgemerkt.

Wenn ein echtes Produkt während Exit einen neuen Öffnungsauftrag erhalten kann, wäre das Abbrechen von Exit und Zurückkehren zu Open eine mögliche Regel. Alternativ kann ein klar definierter Auftrag nach Abschluss neu öffnen. Dafür müssten veraltete Animationsereignisse und Timer unwirksam werden, die neue Absicht erhalten bleiben und der Fokus nachvollziehbar geführt werden. Zwei überlagerte Dialoge oder ein alter Timer, der den gerade neu geöffneten Dialog schließt, wären Fehler.

Dein Selbstcheck

  • Bleibt der Dialog während Exit vorhanden und modal?
  • Nutzen Button und Escape denselben Schließweg?
  • Funktioniert der Abschluss auch ohne Animationsereignis?
  • Wird bei reduzierter Bewegung sofort weitergeschaltet?
  • Bleiben Fokus, Zustand und Einstellung nach erneutem Öffnen konsistent?

DEIN MERKSATZ

Bewegung soll den Wechsel erklären, nicht die Aufgabe bremsen.

Animation ist sinnvoll, wenn sie Orientierung erhält oder Feedback liefert. Eine schöne Animation, die bei einer häufigen Aufgabe unnötig warten lässt, ist kein gutes Interaction Design.