Was macht motion?
Dieser Skill erstellt Motion. Er erweitert den Motion-Abschnitt von product-ui/references/values.md, der die festgelegten Zahlen (Easing-Kurven, Press-Skala, Bounce, Staffelung, die Dauertabelle, Toast- und Layout-Dauern, Reisedistanzen, das Exit-zu-Entrance-Verhältnis) enthält. Diese Zahlen werden hier nicht wiederholt; dieser Skill fügt die Reihenfolge der Entscheidungen, die Tokens, die Rezepte und die Fehlermodi hinzu.
Vor jeder Entscheidung wende product-ui §1 (das Geschmacksjournal und die Priorität) und §5 (welcher Skill wofür zuständig ist) an.
Die Entscheidungsreihenfolge
Gehe diese Schritte von oben nach unten durch. Jeder Schritt kann die Aufgabe beenden. Null Zeilen Animation ist ein gültiges Ergebnis, und du sagst es.
1. Soll es sich bewegen? Das Frequenz-Gate
Frage, wie oft ein Benutzer dies pro Tag sieht. Die Stufen sind in values.md. In der Praxis:
- Initiiert durch Tastatur oder 100+ Mal pro Tag gesehen: keine Bewegung. Warum: Wiederholte Bewegung wird zu Verzögerung.
- Dutzende Male pro Tag gesehen (Hover, Tab-Wechsel, Zeilenauswahl): keine oder ein sofortiger Farbwechsel.
- Daten, die der Benutzer liest oder bearbeitet (eine Zahl in einer Tabelle, ein Diagramm in einem Live-Dashboard) bewegen sich nie aus Stilgründen. Warum: Bewegung zieht den Blick von der gelesenen Sache ab.
- Wenn das Gate Nein sagt, schreibe keine Animation. Sage, welche Stufe es getroffen hat, und biete die statische Alternative an (einen sofortigen Zustandswechsel).
2. Benenne den Zweck
Wähle ein Wort: räumlich (woher es kam oder wohin es ging), Zustand (das hat sich geändert), Feedback (ich habe dich gehört), Kontinuität (verhindert einen Sprung), Erklärung (nur seltene Momente). Kann keins benennen: stop. Warum: „Das sieht nett aus“ bei einem häufigen Element ist die Hauptquelle für sich langsam anfühlende UI.
3. Wähle das Werkzeug: das günstigste, das funktioniert
| Bedarf | Werkzeug |
|---|---|
| Ein Zustand, der durch Klasse, Attribut, Hover oder Drücken umgeschaltet wird | CSS-Transition |
Eintritt aus display: none, Popover oder Dialog | @starting-style + transition-behavior: allow-discrete |
| Eine einmalige Sequenz, Staffelung oder Bewegung, die laufen muss, während die Seite beschäftigt ist | CSS @keyframes |
JS entscheidet wann; du brauchst abbrechen, umkehren, finished oder FLIP | WAAPI el.animate() |
| Federn, Geschwindigkeitsübergabe, Exit eines unmountenden React-Knotens, geteiltes Layout | Motion (motion/react) |
Wenn das Ding eine ganze Komponente ist (Menü, Toast, Drawer), verwende eine Bibliothek, die Fokusbehandlung mitliefert, und frage pick-library. Installiere niemals Motion für einen Fade. Warum: CSS läuft außerhalb des Hauptthreads und kostet kein Bundle.
4. Eigenschaften
- Animiere
transformundopacity;filternur für einen ≤ 2px Austausch-Blur. Nenne jede Eigenschaft; niemalstransition: all. - Höhen- und Layoutänderungen: siehe
references/recipes.md(Grid-Zeilen, FLIP). Animiere niemalswidth,top,leftodermargin, um etwas zu bewegen. Warum: Jede erzwingt ein Layout pro Frame. - Animiere niemals
box-shadow. Fade stattdessen dieopacityeines Pseudoelements. translate-Prozentsätze sind relativ zur eigenen Größe des Elements, daher passttranslateY(100%)für Drawer und Toasts jeder Höhe.
5. Kurve und Dauer, abgestimmt auf die Verwendung
Wähle niemals die Zahl, die deiner Vermutung am nächsten kommt. Gib die Verwendung an („Menü schließen“, „Icon-Tausch“), nimm dann diese Zeile aus references/tokens.md. Wenn keine Zeile mit der Verwendung übereinstimmt, füge ein neues Token hinzu, das nach der Verwendung benannt ist; verbiege kein vorhandenes. Warum: Ein Token ist eine Aussage darüber, wofür die Bewegung gedacht ist, daher ist eine ähnliche Zahl mit der falschen Verwendung das falsche Token.
- Eintreten oder Verlassen:
--ease-out. Bewegung auf dem Bildschirm:--ease-in-out. Drawer:--ease-drawer. Farbe:ease. Spinner:linear. Niemalsease-in. - Alles, was ziehbar oder während des Flugs unterbrechbar ist, verwendet eine Feder, keine Kurve. Warum: Eine Kurve kann die Geschwindigkeit, die die Hand des Benutzers hatte, nicht aufnehmen.
- Langsames Gefühl: zuerst die Dauer kürzen, dann die Distanz, und erst dann die Kurve anfassen.
6. Unterbrechung
- Alles, was der Benutzer zweimal pro Sekunde auslösen kann, ist eine Transition oder eine Feder, niemals
@keyframes. Warum: Eine Transition zielt neu auf den aktuellen Wert; Keyframes starten neu und springen. - WAAPI: Breche die alte Animation nach dem Auslesen des aktuellen Werts ab und starte die neue von diesem Wert aus (
references/recipes.md). - Ein 10 Mal gespammter Umschalter muss im richtigen Zustand enden, ohne zu springen. Teste es.
7. Exit
- Der Standard-Exit ist ein Fade, kürzer als der Eintritt (die Tokens-Datei hat jedes Paar).
- Alles mit einer Richtung (Drawer, Sheet, weggeschobener Toast) verlässt den Weg, den es gekommen ist.
- Das verlassende Element hört sofort auf, Platz zu belegen, sodass Geschwister die Lücke schließen, während es verblasst.
- Exits sind gleichzeitig und niemals gestaffelt. Warum: Der Benutzer hat bereits entschieden zu gehen.
8. Reduced Motion, mitgeliefert mit der Animation
Deckkraft und Farbe bleiben. Folien, Skalierung, Unschärfe und Bounce verschwinden; Folien werden zu Fades, Bounce wird zu 0. Verwende niemals Bewegung als einzigen Hinweis. Muster in references/recipes.md.
Choreografie
Besetze jedes sich bewegende Element, bevor du es zeitlich abstimmst:
| Rolle | Behandlung |
|---|---|
| Lead (das eine Ding, das beachtet werden soll) | Bewegt sich zuerst, reist am weitesten |
| Support | Folgt, reist weniger |
| Dekoration | Verblasst an Ort und Stelle, oder nichts |
| Statisch (Container, Chrome) | Bewegt sich nie |
- Ein Eintritt pro Container. Wenn das Panel hineingleitet, ist sein Inhalt bereits da. Warum: Zwei geschichtete Eintritte lassen den Benutzer zweimal warten.
- Reihenfolge nach Wichtigkeit, nicht nach DOM-Reihenfolge. Die Verzögerung bleibt bei den values.md-Schritten (40ms zwischen Elementen, 100ms zwischen Titel, Text und Aktionen). Variiere Distanz und Dauer nach Rolle anstelle der Verzögerung.
- Begrenze eine Listenstaffelung auf Index 6 (eine letzte Verzögerung von 240ms); Elemente danach teilen sich diese Verzögerung. Die Eingabe funktioniert, während sie abgespielt wird.
- Staffele niemals ein Menü, eine Befehlspalette, Tab-Inhalte oder irgendein wiederholt geöffnetes Element.
- Bleib still: alles, was der Benutzer gleich anklicken wird, alles, was bereits auf dem Bildschirm ist, wenn die Seite erneut geladen wird, und ein Update, das innerhalb von 150ms nach dem letzten eintrifft (springe zum Endzustand).
- Richtung folgt der Bedeutung: Nächstes tritt von rechts ein, Zurück von links; Dinge, die der Benutzer ausgelöst hat, wachsen vom Auslöser nach außen.
Bei Bedarf laden
- references/tokens.md: laden für jede genaue Dauer, Distanz, Skalierung, Unschärfe, Easing- oder Federwert und für die Tokenisierung einer vorhandenen Codebasis.
- references/recipes.md: laden beim Erstellen eines Menüs, Popovers, Dialogs, Drawers, Toasts, Listeneintritts, Icon-Tauschs, Höhenänderung, Tab-Indikators oder Zahlenänderung.
- references/gestures.md: laden für Drag, Wisch, Flick, Sheets oder alles, was einem Zeiger folgt.
- references/performance.md: laden, wenn Bewegung Frames fallen lässt, bevor man zu
will-changegreift, und wenn Updates schnell eintreffen.
Paare
| Kaputt | Gefixt | Warum |
|---|---|---|
.menu { transition: all 300ms ease-in } | transition: opacity 200ms, transform 200ms var(--ease-out) | Benannte Eigenschaften; ease-in ist träge; Menüs ≤ 200ms |
scale(0) zu scale(1) | scale(0.95) und opacity: 0 zu 1 | Nichts erscheint aus nichts |
| Popover skaliert von seiner Mitte | transform-origin: var(--transform-origin) | Es sollte aus seinem Auslöser herauswachsen |
.open { animation: slide-in 300ms } | transition: transform auf dem umgeschalteten Zustand | Keyframes schnappen, wenn sie unterbrochen werden |
Jede Zeile animation-delay: i * 120ms | min(i, 6) * 40ms | Die letzte Zeile kommt unter 300ms an |
| Drawer verblasst nach dem Hineingleiten | Er gleitet auf dem Weg zurück, den er gekommen ist | Richtung ist die Erklärung |
Drag-Loslassen auf einem festen ease-out | Eine Feder, gefüttert mit der Loslassgeschwindigkeit | Der Schwung der Hand setzt sich fort |
setY(y) in pointermove | el.style.transform = ... | Kein Neu-Rendern pro Frame |
Bevor du fertig bist
Durchsuche den Diff, dann überprüfe.
| Suche nach | Behebung |
|---|---|
transition: all, will-change auf *, ease-in | Benannte Eigenschaften, entfernen, --ease-out |
scale(0), transform-origin: center auf einem Popover | 0.95, der Auslöser-Ursprung (Dialoge bleiben zentriert) |
@keyframes auf einer Klasse, die umschaltet | Eine Transition |
| Eine Dauer über ihrer Zeile in tokens.md, oder eine Zahl nicht in tokens.md | Das Token der Verwendung |
Bewegung auf keydown-Handlern oder einer Palette | Entferne es |
width, height, top, left, box-shadow in einer Transition | transform, opacity oder das Rezept |
Kein prefers-reduced-motion-Pfad | Füge ihn hinzu |
Überprüfe in der laufenden App: spiele es mit 4× ab (DevTools Animations Panel, oder multipliziere Dauern), spamme jeden Umschalter, teste mit eingeschaltetem Reduced Motion und drossle die CPU um das 4-fache. Sage, welche dieser Tests du nicht ausführen konntest.