diff --git a/docs/examples/animate-node-position/App.vue b/docs/examples/animate-node-position/App.vue
new file mode 100644
index 000000000..25ff995f4
--- /dev/null
+++ b/docs/examples/animate-node-position/App.vue
@@ -0,0 +1 @@
+<script setup></script>
diff --git a/docs/examples/animate-node-position/useAnimateNode.js b/docs/examples/animate-node-position/useAnimateNode.js
new file mode 100644
index 000000000..cddd1f90c
--- /dev/null
+++ b/docs/examples/animate-node-position/useAnimateNode.js
@@ -0,0 +1,62 @@
+/**
+ * @typedef {object} Node
+ * @property {{ x: number, y: number }} position - The current position of the node.
+ */
+
+/**
+ * @typedef {object} XYPosition
+ * @property {number} x - The target x-coordinate.
+ * @property {number} y - The target y-coordinate.
+ */
+
+/**
+ * @typedef {object} AnimateOptions
+ * @property {number} [duration=300] - Total time for the transition in milliseconds.
+ * @property {(t: number) => number} [easing] - Easing function that maps a normalized time (0 to 1)
+ *                                              to another value (0 to 1). Defaults to linear if not provided.
+ * @property {() => void} [onFinished] - Callback fired when the transition completes.
+ * @property {(position: XYPosition) => void} [onAnimate] - Callback fired on each animation frame.
+ */
+
+/**
+ * Creates a function that animates a node from its current position to a given target position.
+ *
+ * @returns {(node: Node, position: XYPosition, options?: AnimateOptions) => void}
+ *          A function that, when invoked, starts the animation.
+ */
+export function useAnimateNode() {
+  return (node, position, options = {}) => {
+    const startX = node.position.x
+    const startY = node.position.y
+    const endX = position.x
+    const endY = position.y
+
+    const duration = typeof options.duration === 'number' ? options.duration : 300
+    const easing = typeof options.easing === 'function' ? options.easing : (t) => t
+    const startTime = performance.now()
+
+    /**
+     * Runs one animation frame, updating the node's position based on the elapsed time.
+     */
+    function animate() {
+      const currentTime = performance.now()
+      const elapsed = currentTime - startTime
+      const t = Math.min(elapsed / duration, 1)
+      const easedT = easing(t)
+
+      options.onAnimate({
+        x: startX + (endX - startX) * easedT,
+        y: startY + (endY - startY) * easedT,
+      })
+
+      if (t < 1) {
+        requestAnimationFrame(animate)
+      } else if (typeof options.onFinished === 'function') {
+        options.onFinished()
+      }
+    }
+
+    // Start the animation
+    requestAnimationFrame(animate)
+  }
+}
diff --git a/docs/examples/index.ts b/docs/examples/index.ts
index 9504f6874..e1790e569 100644
--- a/docs/examples/index.ts
+++ b/docs/examples/index.ts
@@ -18,7 +18,17 @@ import { IntersectionApp, IntersectionCSS } from './intersection'
 import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
 import { NodeResizerApp, ResizableNode } from './node-resizer'
 import { ToolbarApp, ToolbarNode } from './node-toolbar'
-import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
+import {
+  LayoutApp,
+  LayoutEdge,
+  LayoutElements,
+  LayoutIcon,
+  LayoutNode,
+  useAnimateNode,
+  useLayout,
+  useRunProcess,
+  useShuffle,
+} from './layout'
 import { SimpleLayoutApp, SimpleLayoutElements, SimpleLayoutIcon, useSimpleLayout } from './layout-simple'
 import { LoopbackApp, LoopbackCSS, LoopbackEdge } from './loopback'
 import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
@@ -137,6 +147,7 @@ export const exampleImports = {
     'useRunProcess.js': useRunProcess,
     'useShuffle.js': useShuffle,
     'useLayout.js': useLayout,
+    'useAnimateNode.js': useAnimateNode,
     'Icon.vue': LayoutIcon,
     'additionalImports': {
       '@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
diff --git a/docs/examples/layout/App.vue b/docs/examples/layout/App.vue
index 766f77f2c..d2fa08fde 100644
--- a/docs/examples/layout/App.vue
+++ b/docs/examples/layout/App.vue
@@ -1,6 +1,6 @@
 <script setup>
 import { nextTick, ref } from 'vue'
-import { Panel, VueFlow, useVueFlow } from '@vue-flow/core'
+import { Panel, VueFlow } from '@vue-flow/core'
 import { Background } from '@vue-flow/background'
 import Icon from './Icon.vue'
 import ProcessNode from './ProcessNode.vue'
@@ -17,14 +17,14 @@ const edges = ref(initialEdges)
 
 const cancelOnError = ref(true)
 
+const animatePositionChange = ref(false)
+
 const shuffle = useShuffle()
 
 const { graph, layout, previousDirection } = useLayout()
 
 const { run, stop, reset, isRunning } = useRunProcess({ graph, cancelOnError })
 
-const { fitView } = useVueFlow()
-
 async function shuffleGraph() {
   await stop()
 
@@ -42,19 +42,15 @@ async function layoutGraph(direction) {
 
   reset(nodes.value)
 
-  nodes.value = layout(nodes.value, edges.value, direction)
-
-  nextTick(() => {
-    fitView()
-  })
+  await layout(nodes.value, edges.value, direction, animatePositionChange.value)
 }
 </script>
 
 <template>
   <div class="layout-flow">
     <VueFlow
-      :nodes="nodes"
-      :edges="edges"
+      v-model:nodes="nodes"
+      v-model:edges="edges"
       :default-edge-options="{ type: 'animation', animated: true }"
       @nodes-initialized="layoutGraph('LR')"
     >
@@ -105,6 +101,11 @@ async function layoutGraph(direction) {
           <label>Cancel on error</label>
           <input v-model="cancelOnError" type="checkbox" />
         </div>
+
+        <div class="checkbox-panel">
+          <label>Animate position change</label>
+          <input v-model="animatePositionChange" type="checkbox" />
+        </div>
       </Panel>
     </VueFlow>
   </div>
diff --git a/docs/examples/layout/index.ts b/docs/examples/layout/index.ts
index 75b3a14c5..6c2c992d3 100644
--- a/docs/examples/layout/index.ts
+++ b/docs/examples/layout/index.ts
@@ -6,3 +6,4 @@ export { default as useRunProcess } from './useRunProcess.js?raw'
 export { default as useShuffle } from './useShuffle.js?raw'
 export { default as useLayout } from './useLayout.js?raw'
 export { default as LayoutIcon } from './Icon.vue?raw'
+export { default as useAnimateNode } from './useAnimateNode.js?raw'
diff --git a/docs/examples/layout/useAnimateNode.js b/docs/examples/layout/useAnimateNode.js
new file mode 100644
index 000000000..cddd1f90c
--- /dev/null
+++ b/docs/examples/layout/useAnimateNode.js
@@ -0,0 +1,62 @@
+/**
+ * @typedef {object} Node
+ * @property {{ x: number, y: number }} position - The current position of the node.
+ */
+
+/**
+ * @typedef {object} XYPosition
+ * @property {number} x - The target x-coordinate.
+ * @property {number} y - The target y-coordinate.
+ */
+
+/**
+ * @typedef {object} AnimateOptions
+ * @property {number} [duration=300] - Total time for the transition in milliseconds.
+ * @property {(t: number) => number} [easing] - Easing function that maps a normalized time (0 to 1)
+ *                                              to another value (0 to 1). Defaults to linear if not provided.
+ * @property {() => void} [onFinished] - Callback fired when the transition completes.
+ * @property {(position: XYPosition) => void} [onAnimate] - Callback fired on each animation frame.
+ */
+
+/**
+ * Creates a function that animates a node from its current position to a given target position.
+ *
+ * @returns {(node: Node, position: XYPosition, options?: AnimateOptions) => void}
+ *          A function that, when invoked, starts the animation.
+ */
+export function useAnimateNode() {
+  return (node, position, options = {}) => {
+    const startX = node.position.x
+    const startY = node.position.y
+    const endX = position.x
+    const endY = position.y
+
+    const duration = typeof options.duration === 'number' ? options.duration : 300
+    const easing = typeof options.easing === 'function' ? options.easing : (t) => t
+    const startTime = performance.now()
+
+    /**
+     * Runs one animation frame, updating the node's position based on the elapsed time.
+     */
+    function animate() {
+      const currentTime = performance.now()
+      const elapsed = currentTime - startTime
+      const t = Math.min(elapsed / duration, 1)
+      const easedT = easing(t)
+
+      options.onAnimate({
+        x: startX + (endX - startX) * easedT,
+        y: startY + (endY - startY) * easedT,
+      })
+
+      if (t < 1) {
+        requestAnimationFrame(animate)
+      } else if (typeof options.onFinished === 'function') {
+        options.onFinished()
+      }
+    }
+
+    // Start the animation
+    requestAnimationFrame(animate)
+  }
+}
diff --git a/docs/examples/layout/useLayout.js b/docs/examples/layout/useLayout.js
index 652aced6d..ef4b5ee6e 100644
--- a/docs/examples/layout/useLayout.js
+++ b/docs/examples/layout/useLayout.js
@@ -1,53 +1,98 @@
 import dagre from '@dagrejs/dagre'
-import { Position, useVueFlow } from '@vue-flow/core'
-import { ref } from 'vue'
+import { Position, getRectOfNodes, useVueFlow } from '@vue-flow/core'
+import { nextTick, ref } from 'vue'
+import { useAnimateNode } from './useAnimateNode'
 
 /**
  * Composable to run the layout algorithm on the graph.
  * It uses the `dagre` library to calculate the layout of the nodes and edges.
  */
 export function useLayout() {
-  const { findNode } = useVueFlow()
+  const { findNode, updateNode, fitView, fitBounds } = useVueFlow()
+
+  const animateNode = useAnimateNode()
 
   const graph = ref(new dagre.graphlib.Graph())
 
   const previousDirection = ref('LR')
 
-  function layout(nodes, edges, direction) {
-    // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
-    const dagreGraph = new dagre.graphlib.Graph()
+  async function layout(nodes, edges, direction, animate = false) {
+    return new Promise((resolve) => {
+      // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
+      const dagreGraph = new dagre.graphlib.Graph()
+
+      graph.value = dagreGraph
+
+      dagreGraph.setDefaultEdgeLabel(() => ({}))
+
+      const isHorizontal = direction === 'LR'
+      dagreGraph.setGraph({ rankdir: direction })
+
+      previousDirection.value = direction
+
+      for (const node of nodes) {
+        // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
+        const graphNode = findNode(node.id)
 
-    graph.value = dagreGraph
+        dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
+      }
+
+      for (const edge of edges) {
+        dagreGraph.setEdge(edge.source, edge.target)
+      }
 
-    dagreGraph.setDefaultEdgeLabel(() => ({}))
+      dagre.layout(dagreGraph)
 
-    const isHorizontal = direction === 'LR'
-    dagreGraph.setGraph({ rankdir: direction })
+      if (animate) {
+        const nodesWithPosition = [...nodes].map((node) => {
+          const nodeWithPosition = dagreGraph.node(node.id)
+          const position = { x: nodeWithPosition.x, y: nodeWithPosition.y }
 
-    previousDirection.value = direction
+          return {
+            ...node,
+            position,
+            computedPosition: position,
+          }
+        })
+
+        const nodesRect = getRectOfNodes(nodesWithPosition)
+
+        fitBounds(nodesRect, { duration: 1000 })
+      }
 
-    for (const node of nodes) {
-      // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
-      const graphNode = findNode(node.id)
+      // set nodes with updated positions
+      nodes.forEach((node) => {
+        const nodeWithPosition = dagreGraph.node(node.id)
+        const position = { x: nodeWithPosition.x, y: nodeWithPosition.y }
 
-      dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
-    }
+        updateNode(node.id, {
+          targetPosition: isHorizontal ? Position.Left : Position.Top,
+          sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
+        })
 
-    for (const edge of edges) {
-      dagreGraph.setEdge(edge.source, edge.target)
-    }
+        if (animate) {
+          animateNode(node, position, {
+            duration: 1000,
+            onAnimate: (animatedPosition) => {
+              updateNode(node.id, { position: animatedPosition })
+            },
+            onFinished: () => {
+              updateNode(node.id, { position })
 
-    dagre.layout(dagreGraph)
+              resolve(true)
+            },
+          })
+        } else {
+          updateNode(node.id, { position })
+        }
+      })
 
-    // set nodes with updated positions
-    return nodes.map((node) => {
-      const nodeWithPosition = dagreGraph.node(node.id)
+      if (!animate) {
+        nextTick(() => {
+          fitView()
+        })
 
-      return {
-        ...node,
-        targetPosition: isHorizontal ? Position.Left : Position.Top,
-        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
-        position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
+        resolve(true)
       }
     })
   }