diff --git a/.claude/plugins/n8n/skills/design-system/SKILL.md b/.claude/plugins/n8n/skills/design-system/SKILL.md
index d07a1472338..eecdfa4ae2b 100644
--- a/.claude/plugins/n8n/skills/design-system/SKILL.md
+++ b/.claude/plugins/n8n/skills/design-system/SKILL.md
@@ -19,12 +19,15 @@ Reference these guidelines when:
- Follow guidelines in `packages/frontend/@n8n/design-system/src/styleguide/*.mdx`
- ALWAYS use CSS variables for styles from `packages/frontend/@n8n/design-system/src/css/_tokens.scss` or `packages/frontend/@n8n/design-system/src/css/_primtivies.scss`. Use hard-coded values only when no suitable tokens.
- ALWAYS prefer using existing components from `packages/frontend/@n8n/design-system/src/components`. Prefer components that aren't marked `@deprecated`.
-- When reviewing UI changes or adding new components, follow `rules/web-interface-guidelines.md`
- Use `light-dark()` when alternating colors for ligh/dark mode
+- When working with animations or transitions, ALWAYS prefer using mixins from `packages/frontend/@n8n/design-system/src/css/mixins/motion.scss`
+- When reviewing animations, follow the guides in `rules/web-animation-guidelines.md`
+- When reviewing UI changes or adding new components, follow `rules/web-interface-guidelines.md`
## Examples
- "Add a modal dialog for confirming workflow deletion" → Use `N8nDialog`
- "Add a dropdown to select workflow status" → Use `N8nDropdown` or `N8nSelect`
- "Add button with + icon to add new tiem" → Wrap `N8nButton` with `iconOnly` prop with `N8nTooltip` and wrap in `N8nTooltip`. Use `N8nIcon` and proper aria-label.
- "Add a destructive action button" → use `N8nButton` with `variant="destructive"`
-- Make background color white/black → Use `var(--background--surface)` for white on light mode and "black" on dark mode
+- "Make background color white/black" → Use `var(--background--surface)` for white on light mode and "black" on dark mode
+- "Animate the title in gracefully" -> Use `fade-in-up` mixin from `motion.scss` with `var(--duration--base)`
diff --git a/.claude/plugins/n8n/skills/design-system/rules/web-animation-guidelines.md b/.claude/plugins/n8n/skills/design-system/rules/web-animation-guidelines.md
new file mode 100644
index 00000000000..0b377bd7dfe
--- /dev/null
+++ b/.claude/plugins/n8n/skills/design-system/rules/web-animation-guidelines.md
@@ -0,0 +1,93 @@
+# Web Motion Guidelines
+Design and implement web animations that feel natural and purposeful
+
+## Timing and Duration
+
+## Duration Guidelines
+
+| Element Type | Duration |
+| --------------------------------- | --------- |
+| Micro-interactions | 100-150ms |
+| Standard UI (tooltips, dropdowns) | 150-250ms |
+| Modals, drawers | 200-300ms |
+
+**Rules:**
+
+- UI animations should stay under 300ms
+- Larger elements animate slower than smaller ones
+- Exit animations can be ~20% faster than entrance
+- Match duration to distance - longer travel = longer duration
+
+### The Frequency
+
+Determine how often users will see the animation:
+
+- **100+ times/day** → No animation (or drastically reduced)
+- **Occasional use** → Standard animation
+- **Rare/first-time** → Can be more special
+
+**Example:** Raycast never animates because users open it hundreds of times a day.
+
+## When to Animate
+
+**Do animate:**
+
+- Enter/exit transitions for spatial consistency
+- State changes that benefit from visual continuity
+- Responses to user actions (feedback)
+- Rarely-used interactions where delight adds value
+
+**Don't animate:**
+
+- Keyboard-initiated actions
+- Hover effects on frequently-used elements
+- Anything users interact with 100+ times daily
+- When speed matters more than smoothness
+
+## Performance
+
+Prefer animating `transform` and `opacity`. These skip layout and paint stages, running entirely on the GPU.
+
+**Avoid animating:**
+
+- `padding`, `margin`, `height`, `width` (trigger layout)
+- `blur` filters above 20px (expensive, especially Safari)
+- CSS variables in deep component trees
+
+### Optimization Techniques
+
+```css
+/* Force GPU acceleration */
+.animated-element {
+ will-change: transform;
+}
+```
+
+## Practical Tips
+
+Quick reference for common scenarios. See [PRACTICAL-TIPS.md](PRACTICAL-TIPS.md) for detailed implementations.
+
+| Scenario | Solution |
+| ------------------------------- | ----------------------------------------------- |
+| Make buttons feel responsive | Add `transform: scale(0.97)` on `:active` |
+| Element appears from nowhere | Start from `scale(0.95)`, not `scale(0)` |
+| Shaky/jittery animations | Add `will-change: transform` |
+| Hover causes flicker | Animate child element, not parent |
+| Popover scales from wrong point | Set `transform-origin` to trigger location |
+| Sequential tooltips feel slow | Skip delay/animation after first tooltip |
+| Small buttons hard to tap | Use 44px minimum hit area (pseudo-element) |
+| Something still feels off | Add subtle blur (under 20px) to mask it |
+| Hover triggers on mobile | Use `@media (hover: hover) and (pointer: fine)` |
+
+## Easing Decision Flowchart
+
+Is the element entering or exiting the viewport?
+├── Yes → ease-out
+└── No
+├── Is it moving/morphing on screen?
+│ └── Yes → ease-in-out
+└── Is it a hover change?
+├── Yes → ease
+└── Is it constant motion?
+├── Yes → linear
+└── Default → ease-out
diff --git a/packages/@n8n/stylelint-config/src/rules/css-var-naming.ts b/packages/@n8n/stylelint-config/src/rules/css-var-naming.ts
index 18bed5c870e..ab21b124640 100644
--- a/packages/@n8n/stylelint-config/src/rules/css-var-naming.ts
+++ b/packages/@n8n/stylelint-config/src/rules/css-var-naming.ts
@@ -16,6 +16,7 @@ const meta = {
// NOTE: color--text, color--background, color--foreground use double dashes
// to separate "color" from the subtype (text/background/foreground)
const PROPERTY_VOCABULARY = new Set([
+ 'animation',
'color',
'color--text',
'color--background',
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ThinkingMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ThinkingMessage.vue
index d0efbf67e53..45bbb847638 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ThinkingMessage.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ThinkingMessage.vue
@@ -78,7 +78,7 @@ function getIconForStatus(status: ChatUI.ThinkingItem['status']) {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
index 36e18b6d40c..ccaeab6e828 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
@@ -127,6 +127,7 @@ const handleClick = (event: MouseEvent) => {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nCollapsiblePanel/CollapsiblePanel.vue b/packages/frontend/@n8n/design-system/src/components/N8nCollapsiblePanel/CollapsiblePanel.vue
index e68a0cf4b5b..8638517772e 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nCollapsiblePanel/CollapsiblePanel.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nCollapsiblePanel/CollapsiblePanel.vue
@@ -80,6 +80,8 @@ const isOpen = computed({
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nDropdown/Dropdown.vue b/packages/frontend/@n8n/design-system/src/components/N8nDropdown/Dropdown.vue
index 1abaa00f702..440d86ea1db 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nDropdown/Dropdown.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nDropdown/Dropdown.vue
@@ -129,6 +129,8 @@ defineExpose({
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/Icon.vue b/packages/frontend/@n8n/design-system/src/components/N8nIcon/Icon.vue
index cca3e49522c..f70c62270ac 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/Icon.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/Icon.vue
@@ -125,6 +125,8 @@ const resolvedComponent = computed(
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nPulse/Pulse.vue b/packages/frontend/@n8n/design-system/src/components/N8nPulse/Pulse.vue
index 5641ce47cef..3470c3ab112 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nPulse/Pulse.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nPulse/Pulse.vue
@@ -13,19 +13,7 @@ defineOptions({ name: 'N8nPulse' });
diff --git a/packages/frontend/@n8n/design-system/src/css/_primitives.scss b/packages/frontend/@n8n/design-system/src/css/_primitives.scss
index cffbd6b27c0..0991467970c 100644
--- a/packages/frontend/@n8n/design-system/src/css/_primitives.scss
+++ b/packages/frontend/@n8n/design-system/src/css/_primitives.scss
@@ -291,6 +291,9 @@
--duration--snappy: 200ms;
--duration--base: 400ms;
+ --duration--slow: 1000ms;
+ --duration--slowest: 1500ms;
+
--easing--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--easing--ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);
--easing--ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
diff --git a/packages/frontend/@n8n/design-system/src/css/mixins/animations.scss b/packages/frontend/@n8n/design-system/src/css/mixins/animations.scss
deleted file mode 100644
index 11965a03c64..00000000000
--- a/packages/frontend/@n8n/design-system/src/css/mixins/animations.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-@mixin shimmer {
- background: linear-gradient(135deg, #fff, #5e5e5e, #fff);
- background-clip: text;
- color: transparent;
- background-size: 200% 100%;
- animation: shimmer 3.33s linear infinite;
-}
-
-@keyframes shimmer {
- 0% {
- background-position: 200% 0;
- }
- 100% {
- background-position: -200% 0;
- }
-}
diff --git a/packages/frontend/@n8n/design-system/src/css/mixins/motion.scss b/packages/frontend/@n8n/design-system/src/css/mixins/motion.scss
new file mode 100644
index 00000000000..ba6d6e80b60
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/css/mixins/motion.scss
@@ -0,0 +1,490 @@
+// Shimmer - animated gradient for loading/skeleton states
+@mixin shimmer {
+ background: linear-gradient(
+ 135deg,
+ var(--animation--shimmer--background, var(--background--surface)),
+ var(--animation--shimmer--foreground, var(--text-color--subtle)),
+ var(--animation--shimmer--background, var(--background--surface))
+ );
+ background-size: 200% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shimmer var(--animation--shimmer--duration, var(--duration--slow)) infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+// Spin - continuous 360° rotation for loading indicators
+@mixin spin {
+ animation: spin var(--animation--spin--duration, var(--duration--base)) linear infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+// Skeleton pulse - opacity animation for skeleton loading states
+@mixin skeleton-pulse {
+ animation: skeleton-pulse var(--animation--skeleton-pulse--duration, var(--duration--slowest))
+ ease-in-out infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@mixin opacity-pulse {
+ animation: opacityPulse var(--animation--opacity-pulse--duration, var(--duration--slow))
+ var(--animation--opacity-pulse--easing, ease-in-out) infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@mixin popover-in {
+ animation: popoverIn var(--animation--popover-in--duration, var(--duration--snappy))
+ var(--animation--popover-in--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@mixin collapsible-slide-down {
+ animation: collapsibleSlideDown
+ var(--animation--collapsible-slide--duration, var(--duration--snappy))
+ var(--animation--collapsible-slide--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@mixin collapsible-slide-up {
+ animation: collapsibleSlideUp
+ var(--animation--collapsible-slide--duration, var(--duration--snappy))
+ var(--animation--collapsible-slide--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+// Fade in - simple opacity fade for entrance animations
+@mixin fade-in {
+ animation: fadeIn var(--animation--fade-in--duration, var(--duration--snappy))
+ var(--easing--ease-out);
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+// Keyframes
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes skeleton-pulse {
+ 0%,
+ 100% {
+ opacity: var(--animation--skeleton-pulse--opacity-start, 1);
+ }
+ 50% {
+ opacity: var(--animation--skeleton-pulse--opacity-end, 0.4);
+ }
+}
+
+@keyframes opacityPulse {
+ 0%,
+ 100% {
+ opacity: var(--animation--opacity-pulse--opacity-start, 1);
+ }
+ 50% {
+ opacity: var(--animation--opacity-pulse--opacity-end, 0.4);
+ }
+}
+
+@keyframes popoverIn {
+ from {
+ opacity: 0;
+ transform: translate(
+ var(--animation--popover-in--translate-x, 0),
+ var(--animation--popover-in--translate-y, 0)
+ )
+ scale(var(--animation--popover-in--scale, 0.96));
+ }
+ to {
+ opacity: 1;
+ transform: translate(0, 0) scale(1);
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(var(--animation--fade-in--translate, 8px));
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes collapsibleSlideDown {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--reka-collapsible-content-height);
+ }
+}
+
+@keyframes collapsibleSlideUp {
+ from {
+ height: var(--reka-collapsible-content-height);
+ }
+ to {
+ height: 0;
+ }
+}
+
+// Pulse glow - expanding glow effect for loading states (used by N8nPulse)
+@mixin pulse-glow {
+ animation: pulseGlow var(--animation--pulse-glow--duration, 6s) infinite
+ var(--animation--pulse-glow--easing, cubic-bezier(0.33, 1, 0.68, 1));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@mixin pulse-glow-delayed {
+ animation: pulseGlowDelayed var(--animation--pulse-glow--duration, 6s) infinite
+ var(--animation--pulse-glow--easing, cubic-bezier(0.33, 1, 0.68, 1));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes pulseGlow {
+ 0% {
+ box-shadow: 0 0 0 0
+ hsla(var(--color--primary--h), var(--color--primary--s), var(--color--primary--l), 0.4);
+ }
+ 58.33% {
+ box-shadow: 0 0 0 60px
+ hsla(var(--color--primary--h), var(--color--primary--s), var(--color--primary--l), 0);
+ }
+ 66.6% {
+ box-shadow: 0 0 0 66px transparent;
+ }
+ 66.7% {
+ box-shadow: 0 0 0 0 transparent;
+ }
+}
+
+@keyframes pulseGlowDelayed {
+ 0%,
+ 16.66% {
+ box-shadow: 0 0 0 0
+ hsla(var(--color--primary--h), var(--color--primary--s), var(--color--primary--l), 0.4);
+ }
+ 50% {
+ box-shadow: 0 0 0 60px
+ hsla(var(--color--primary--h), var(--color--primary--s), var(--color--primary--l), 0);
+ }
+ 83.3% {
+ box-shadow: 0 0 0 66px transparent;
+ }
+ 83.4% {
+ box-shadow: 0 0 0 0 transparent;
+ }
+}
+
+// Fade animation for simple opacity transitions
+@mixin fade {
+ animation: fade var(--animation--fade--duration, var(--duration--snappy));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fade {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@mixin fade-in-up {
+ animation: fadeInUp var(--animation--fade-in-up--duration, var(--duration--snappy))
+ var(--animation--fade-in-up--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(var(--animation--fade-in-up--translate, var(--spacing--4xs)));
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@mixin fade-in-down {
+ animation: fadeInDown var(--animation--fade-in-down--duration, var(--duration--snappy))
+ var(--animation--fade-in-down--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(
+ calc(-1 * var(--animation--fade-in-down--translate, var(--spacing--4xs)))
+ );
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@mixin fade-in-left {
+ animation: fadeInLeft var(--animation--fade-in-left--duration, var(--duration--snappy))
+ var(--animation--fade-in-left--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(
+ calc(-1 * var(--animation--fade-in-left--translate, var(--spacing--4xs)))
+ );
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@mixin fade-in-right {
+ animation: fadeInRight var(--animation--fade-in-right--duration, var(--duration--snappy))
+ var(--animation--fade-in-right--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeInRight {
+ from {
+ opacity: 0;
+ transform: translateX(var(--animation--fade-in-right--translate, var(--spacing--4xs)));
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@mixin fade-out {
+ animation: fadeOut var(--animation--fade-out--duration, var(--duration--snappy))
+ var(--animation--fade-out--easing, var(--easing--ease-in));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@mixin fade-out-down {
+ animation: fadeOutDown var(--animation--fade-out-down--duration, var(--duration--snappy))
+ var(--animation--fade-out-down--easing, var(--easing--ease-in));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(var(--animation--fade-out-down--translate, var(--spacing--4xs)));
+ }
+}
+
+@mixin fade-out-up {
+ animation: fadeOutUp var(--animation--fade-out-up--duration, var(--duration--snappy))
+ var(--animation--fade-out-up--easing, var(--easing--ease-in));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeOutUp {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(calc(-1 * var(--animation--fade-out-up--translate, var(--spacing--4xs))));
+ }
+}
+
+@mixin fade-out-left {
+ animation: fadeOutLeft var(--animation--fade-out-left--duration, var(--duration--snappy))
+ var(--animation--fade-out-left--easing, var(--easing--ease-in));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeOutLeft {
+ from {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateX(
+ calc(-1 * var(--animation--fade-out-left--translate, var(--spacing--4xs)))
+ );
+ }
+}
+
+@mixin fade-out-right {
+ animation: fadeOutRight var(--animation--fade-out-right--duration, var(--duration--snappy))
+ var(--animation--fade-out-right--easing, var(--easing--ease-in));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes fadeOutRight {
+ from {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateX(var(--animation--fade-out-right--translate, var(--spacing--4xs)));
+ }
+}
+
+@mixin blink-background {
+ animation: blinkBackground var(--animation--blink-background--duration, var(--duration--slow))
+ step-end infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes blinkBackground {
+ from,
+ to {
+ background-color: var(--animation--blink-background--color-start, transparent);
+ }
+ 50% {
+ background-color: var(
+ --animation--blink-background--color-end,
+ var(--color--foreground--shade-2)
+ );
+ }
+}
+
+@mixin typing-blink {
+ animation: typingBlink var(--animation--typing-blink--duration, 1.2s) infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes typingBlink {
+ 0%,
+ 80%,
+ 100% {
+ opacity: var(--animation--typing-blink--opacity-start, 0.35);
+ transform: translateY(0);
+ }
+ 40% {
+ opacity: var(--animation--typing-blink--opacity-end, 1);
+ transform: translateY(var(--animation--typing-blink--translate, -2px));
+ }
+}
+
+@mixin width-transition {
+ transition: width var(--animation--width-transition--duration, var(--duration--snappy))
+ var(--animation--width-transition--easing, var(--easing--ease-out));
+ will-change: width;
+
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ will-change: auto;
+ }
+}
+
+@mixin height-transition {
+ transition: height var(--animation--height-transition--duration, var(--duration--snappy))
+ var(--animation--height-transition--easing, var(--easing--ease-out));
+ will-change: height;
+
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ will-change: auto;
+ }
+}
diff --git a/packages/frontend/@n8n/design-system/src/styleguide/components/MotionExamples.vue b/packages/frontend/@n8n/design-system/src/styleguide/components/MotionExamples.vue
new file mode 100644
index 00000000000..f2590e50fad
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/styleguide/components/MotionExamples.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Open popover
+
+
+ Animated popover surface
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/@n8n/design-system/src/styleguide/motion.mdx b/packages/frontend/@n8n/design-system/src/styleguide/motion.mdx
new file mode 100644
index 00000000000..642f986eaf9
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/styleguide/motion.mdx
@@ -0,0 +1,104 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+import MotionExamples from './components/MotionExamples.vue';
+
+
+
+# Motion
+
+Motion should make n8n feel responsive, understandable, and calm. Use animation to explain
+state changes, guide attention, or show progress. Avoid motion that is decorative, slow,
+distracting, or repeated so often that it gets in the way.
+
+Motion utilities live in `@n8n/design-system/src/css/mixins/motion.scss`.
+
+## Principles
+
+- **Keep motion purposeful**: Use motion when it helps users understand what has changed (e.g loading states, enter/existing surfaces). Don't add motion for the sake of it.
+- **Prefer subtle movement**: Most motion should be short and low-distance. Small transitions, such as opacity and translations, are enough. Don't animate rapid frequently interactions (e.g hover states)
+- **Prefer non re-painting values**: Opacity and transform are the safest animation properties for performance. Use caution with layout and paint properties like height, width, box-shadow, background-color, and filter. When using those properties, add `will-change`.
+- **Always aim for +60fps**: Motion should feel smooth and responsive. If an animation causes jank or lag, simplify the animation or use a more performant approach.
+- **Respect reduced motion**: Always provide a non-animated fallback for users who prefer reduced motion. Motion mixins should handle `prefers-reduced-motion` internally so that consumers can use them without repeating media queries.
+- **Easing**: Use easing to make motion feel more natural. Avoid linear easing for UI interactions, as it can feel mechanical and less responsive. Instead, use `ease-out` for entrances and exits, and `ease-in-out` for movements of already visible elements. Avoid `ease-in` for most UI animations, as it can delay feedback and make the interface feel sluggish.
+
+## Available Mixins
+
+| Mixin | Use for | Notes |
+| ------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
+| `motion.spin` | Loading indicators | Continuous rotation. |
+| `motion.skeleton-pulse` | Skeleton loading placeholders | Use for placeholder surfaces, not interactive elements. |
+| `motion.shimmer` | Text or inline loading states | Best for short-lived AI or thinking states. |
+| `motion.popover-in` | Dropdowns, menus, floating surfaces | Uses opacity, translate, and scale. |
+| `motion.collapsible-slide-down` | Opening collapsible content | Animates height. Use only when height animation is needed. |
+| `motion.collapsible-slide-up` | Closing collapsible content | Animates height. Use only when height animation is needed. |
+| `motion.fade-in` | Simple entrance | Prefer for subtle one-off reveals. |
+| `motion.fade-in-up` | Entrance from below | Use for vertical reveal patterns. |
+| `motion.fade-in-down` | Entrance from above | Use when the element visually comes from above. |
+| `motion.fade-in-left` | Entrance from left | Use for horizontal directional context. |
+| `motion.fade-in-right` | Entrance from right | Use for horizontal directional context. |
+| `motion.fade-out` | Simple exit | Use for removal without spatial movement. |
+| `motion.fade-out-down` | Exit downward | Pair with matching entrance direction when possible. |
+| `motion.fade-out-up` | Exit upward | Pair with matching entrance direction when possible. |
+| `motion.fade-out-left` | Exit left | Pair with matching entrance direction when possible. |
+| `motion.fade-out-right` | Exit right | Pair with matching entrance direction when possible. |
+| `motion.blink-background` | Cursor-like attention states | Avoid for large areas or frequent UI. |
+| `motion.typing-blink` | Typing indicators | Use for small repeated indicators only. |
+| `motion.pulse-glow` | Rare emphasis or loading pulse | Animates `box-shadow`; use sparingly. |
+| `motion.pulse-glow-delayed` | Secondary delayed pulse | Only use with `pulse-glow`. |
+| `motion.width-transition` | Width changes | Layout-affecting. Use cautiously. |
+| `motion.height-transition` | Height changes | Layout-affecting. Use cautiously. |
+
+## Using Mixins
+
+Motion mixins expose CSS variables for local customization. Prefer local overrides over
+creating new keyframes.
+
+```scss
+.dropdown {
+ --animation--popover-in--duration: var(--duration--snappy);
+ --animation--popover-in--translate-y: var(--spacing--4xs);
+
+ @include motion.popover-in;
+}
+```
+
+## Examples
+
+
+
+## Adding New Mixins
+
+Before adding a new mixin, check whether an existing mixin can be customized with CSS
+variables.
+
+Add a new mixin only when:
+
+- The motion pattern is reused by multiple components.
+- The animation has a clear product purpose.
+- The behavior cannot be expressed cleanly with existing mixins.
+- The mixin includes reduced-motion handling.
+- The mixin uses design-system duration and easing tokens.
+
+New mixins should follow this shape:
+
+```scss
+@mixin example-motion {
+ animation: exampleMotion var(--animation--example-motion--duration, var(--duration--snappy))
+ var(--animation--example-motion--easing, var(--easing--ease-out));
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+@keyframes exampleMotion {
+ from {
+ opacity: 0;
+ transform: translateY(var(--animation--example-motion--translate, var(--spacing--4xs)));
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+```
diff --git a/packages/frontend/@n8n/design-system/src/v2/components/Loading/Loading.vue b/packages/frontend/@n8n/design-system/src/v2/components/Loading/Loading.vue
index fda7d16d9cf..9a05a6538c0 100644
--- a/packages/frontend/@n8n/design-system/src/v2/components/Loading/Loading.vue
+++ b/packages/frontend/@n8n/design-system/src/v2/components/Loading/Loading.vue
@@ -84,17 +84,7 @@ function isLastRow(index: number, total: number): boolean {
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatTypingIndicator.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatTypingIndicator.vue
index 2a1b243ea59..61f6b59c799 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatTypingIndicator.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatTypingIndicator.vue
@@ -3,6 +3,8 @@
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonAgentCard.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonAgentCard.vue
index aa2d2a9dd5f..02e449dddae 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonAgentCard.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonAgentCard.vue
@@ -12,6 +12,8 @@
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue
index 5f5523aefcb..2fce9079dbd 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue
@@ -6,6 +6,8 @@
diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/AnimatedCollapsibleContent.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/AnimatedCollapsibleContent.vue
index b7dbca6fba3..66899680420 100644
--- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/AnimatedCollapsibleContent.vue
+++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/AnimatedCollapsibleContent.vue
@@ -9,33 +9,19 @@ import { CollapsibleContent } from 'reka-ui';
diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMessage.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMessage.vue
index 4b774a56758..1609a7793a1 100644
--- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMessage.vue
+++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMessage.vue
@@ -175,6 +175,8 @@ function formatJson(value: unknown): string {