diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -2655,7 +2655,8 @@ fn documents(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
             if d != idx {
                 app.ui.views[d] = v;
                 if ui.input(|i| i.pointer.primary_pressed() && i.pointer.interact_pos().is_some_and(|p| cell.contains(p))) {
-                    app.session.set_active(d);
+                    // Commit any transform tool change before switching documents (#9735)
+                    app.activate_document(d);
                 }
             }
             let stroke = if d == idx { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, Color32::from_black_alpha(160)) };
@@ -2982,8 +2983,11 @@ fn open_tab_clicks(
     cancel_open: Option<photocraft_engine::jobs::JobId>,
 ) {
     if let Some(i) = activate {
-        app.session.set_active(i);
-        app.jobs.focus = None;
+        // Commit any transform tool change before switching documents (#9735)
+        app.activate_document(i);
+        if app.session.active_index() == Some(i) {
+            app.jobs.focus = None;
+        }
     }
     if let Some(job) = focus_open {
         app.jobs.focus = Some(job);
@@ -6909,6 +6913,44 @@ mod tabs_tests;
 
 #[cfg(test)]
 mod tests {
+    /// #9735: Switching documents must commit any open transform session to prevent state loss.
+    #[test]
+    fn transform_commits_on_tab_click() {
+        use egui_kittest::kittest::Queryable;
+        use serde_json::json;
+
+        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
+        for name in ["first", "second"] {
+            app.run("file.new", json!({"width": 64, "height": 64, "name": name})).unwrap();
+        }
+        app.sync_views();
+        app.activate_document(0);
+
+        app.run("select.all", json!({})).unwrap();
+        let ctx = egui::Context::default();
+        crate::menus::invoke(&mut app, &ctx, "select.transformSelection", json!({})).unwrap();
+        assert!(app.ui.transform.is_some(), "transform must be active");
+
+        let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(700.0, 40.0)).build_ui_state(
+            |ui, (app, ready): &mut (PhotocraftApp, bool)| {
+                if *ready {
+                    tabs(app, ui);
+                }
+            },
+            (app, false),
+        );
+
+        PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::Studio);
+        h.state_mut().1 = true;
+        h.run_steps(2);
+
+        h.get_by_label_contains("second").click_button(egui::PointerButton::Primary);
+        h.run_steps(2);
+
+        assert!(h.state().0.ui.transform.is_none(), "transform must clear on tab click");
+        assert_eq!(h.state().0.session.active_index(), Some(1), "active document must change");
+    }
+
     /// #5472: a ⇧ square pressed between pixels snapped to 101 × 102; the short side grows to
     /// match, on the side the drag points to.
     #[test]
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -826,6 +826,16 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result<Value, String> {
     let d = app.ui.close_dialog(id).ok_or_else(|| format!("no dialog {id}"))?;
     match d.kind {
         DialogKind::NewDocument => {
+            // #9735: Before creating a document, we commit the current transform, if there is any
+            if app.ui.transform.is_some() {
+                crate::transform_tool::commit(app);
+
+                // Do not proceed if the transform is not commited
+                if app.ui.transform.is_some() {
+                    return Err("Cannot create file while transform is active".into());
+                }
+            }
+
             let r = app.run("file.new", crate::new_doc_ui::command_params(&d.fields));
             if let Some(i) = app.session.active_index() {
                 app.ui.views[i].fit_pending = true;
diff --git a/crates/ui-egui/src/file_open.rs b/crates/ui-egui/src/file_open.rs
--- a/crates/ui-egui/src/file_open.rs
+++ b/crates/ui-egui/src/file_open.rs
@@ -137,10 +137,22 @@ impl PhotocraftApp {
         // Documents opening in the background are read on the worker too (a 2 GB PSB read
         // would block the window). Preset files (brushes, gradients) go the usual way.
         let ext = std::path::Path::new(path).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
+
+        // #9735: Before opening a document, we commit the current transform, if there is any
+        if !crate::preset_files_ui::PRESET_EXTS.contains(&ext.as_str()) && self.ui.transform.is_some() {
+            crate::transform_tool::commit(self);
+
+            // If the transform commit fails, do not change the document
+            if self.ui.transform.is_some() {
+                return Err("Cannot open file while transform is active".into());
+            }
+        }
+
         if self.background_jobs && !crate::preset_files_ui::PRESET_EXTS.contains(&ext.as_str()) {
             crate::jobs_ui::start_open(self, &display_name(path), Some(path.to_string()), photocraft_engine::jobs::OpenSource::Path(path.to_string()))?;
             return Ok(Vec::new());
         }
+
         let bytes = photocraft_format::read_file(std::path::Path::new(path)).map_err(|e| format!("{path}: {e}"))?;
         self.open_file(path, &bytes)
     }
@@ -190,7 +202,11 @@ impl PhotocraftApp {
         let canon = canonical(path);
         let same = |p: &str| same_file(p, path, canon.as_deref());
         if let Some(i) = self.session.documents().iter().position(|d| d.path.as_deref().is_some_and(same)) {
-            self.session.set_active(i);
+            self.activate_document(i); // Commit any transform tool change before switching documents (#9735)
+            // If the activate_document fails, then do not proceed.
+            if self.session.active_index() != Some(i) {
+                return false;
+            }
             self.jobs.focus = None;
         } else if let Some(o) = self.jobs.opens.iter().find(|o| o.path.as_deref().is_some_and(|p| !is_template(p) && same(p))) {
             // A template still loading opens as another Untitled, as it does once loaded.
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -1620,6 +1620,15 @@ impl PhotocraftApp {
         if let Some(r) = preset_files_ui::open(self, name, bytes) {
             return r.map(|()| Vec::new());
         }
+
+        // #9735: Before opening a document from bytes, commit the current transform if any
+        if self.ui.transform.is_some() {
+            crate::transform_tool::commit(self);
+            if self.ui.transform.is_some() {
+                return Err("Cannot open file while transform is active".into());
+            }
+        }
+
         let name = &self.open_name(name);
         // Decoded on a worker: a tab with progress appears now, the document when it's ready
         // (warnings are shown then).
@@ -2894,6 +2903,23 @@ impl PhotocraftApp {
     }
 }
 
+impl PhotocraftApp {
+    /// This is a helper function created in order to fix #9735.
+    /// Safely switches the active document index, automatically committing any
+    /// active Free Transform or Warp session if the target document differs.
+    pub(crate) fn activate_document(&mut self, index: usize) {
+        if index < self.session.documents().len() && self.session.active_index() != Some(index) && self.ui.transform.is_some() {
+            crate::transform_tool::commit(self);
+
+            // If the transform commit fails, do not change the document
+            if self.ui.transform.is_some() {
+                return;
+            }
+        }
+        self.session.set_active(index);
+    }
+}
+
 impl PhotocraftApp {
     /// Cached `Surface::content_bounds` keyed by an id and the surface's tile identity.
     pub fn cached_bounds(&mut self, key: u64, surface: &photocraft_raster::Surface) -> photocraft_geom::Rect {
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -201,6 +201,7 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
         apply_workspace(app);
         return Ok(json!({"workspace": app.ui.workspace}));
     }
+
     match id {
         // ⌘↩ / Ctrl+Enter (#306): load the path selected in the Paths panel (or the one being
         // drawn) as a selection.
@@ -634,14 +635,22 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
             let n = app.session.documents().len();
             let at = app.session.active_index().ok_or("no document open")?;
             let to = if id == "window.nextDocument" { (at + 1) % n } else { (at + n - 1) % n };
-            app.session.set_active(to);
+            app.activate_document(to); // #9735: We need to commit the transform changes before changing the document
+            // If the activate_document fails, then do not proceed.
+            if app.session.active_index() != Some(to) {
+                return Err("could not activate document".into());
+            }
             app.jobs.focus = None;
             Ok(json!({"document": to}))
         }
         id if let Some(index) = id.strip_prefix(DOCUMENT_ITEM) => {
             let n = app.session.documents().len();
             let to = index.parse::<usize>().ok().filter(|i| *i < n).ok_or("no such document")?;
-            app.session.set_active(to);
+            app.activate_document(to); // #9735: We need to commit the transform changes before changing the document
+            // If the activate_document fails, then do not proceed.
+            if app.session.active_index() != Some(to) {
+                return Err("could not activate document".into());
+            }
             app.jobs.focus = None;
             Ok(json!({"document": to}))
         }
@@ -2367,6 +2376,26 @@ mod window_documents_tests {
         window[at..].iter().map(|i| (i.id.clone(), i.label.clone(), i.checked)).collect()
     }
 
+    /// #9735: Switching documents must commit any open transform session to prevent state loss.
+    #[test]
+    fn transform_commits_on_window_next_document() {
+        use serde_json::json;
+        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({"width": 64, "height": 64})).unwrap();
+        app.run("file.new", json!({"width": 64, "height": 64})).unwrap();
+        app.session.set_active(0);
+
+        app.run("select.all", json!({})).unwrap();
+        let ctx = egui::Context::default();
+        crate::menus::invoke(&mut app, &ctx, "select.transformSelection", json!({})).unwrap();
+        assert!(app.ui.transform.is_some(), "transform must be active on the first document");
+
+        crate::menus::invoke(&mut app, &ctx, "window.nextDocument", json!({})).unwrap();
+
+        assert!(app.ui.transform.is_none(), "transform must clear when switching documents via command");
+        assert_eq!(app.session.active_index(), Some(1), "focus must move to the second document");
+    }
+
     /// #2991: Window ends with the open documents in tab order, the active one checked, as in
     /// Photoshop; choosing one brings it to the front.
     #[test]
diff --git a/crates/ui-egui/src/transform_tool.rs b/crates/ui-egui/src/transform_tool.rs
--- a/crates/ui-egui/src/transform_tool.rs
+++ b/crates/ui-egui/src/transform_tool.rs
@@ -2212,6 +2212,13 @@ fn front_vertices(h: &Homography, n: usize) -> Vec<bool> {
 /// Photoshop. The box, handles and reference point are [`draw_overlay`], drawn over the guides.
 pub fn draw_content(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
     let (Some(t), Some(pv)) = (&app.ui.transform, &app.transform_preview) else { return };
+
+    // Fix for ghosting after #9735: The async background commit leaves the transform preview populated.
+    // We must ensure the preview is only drawn if its document matches the currently active document.
+    if app.session.active().map(|st| st.doc.id) != Some(pv.doc.id) {
+        return;
+    }
+
     let output = crate::monitor_status::view_display(app, painter.ctx());
     let (display, _) = crate::canvas::canvas_display(app, &pv.doc, output);
     // Selection tint and lone mask/channel values are UI diagnostics, not document colors.
@@ -2272,6 +2279,13 @@ pub fn draw_content(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform
 /// drew; the warp grid and its split guides for a warp.
 pub fn draw_overlay(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
     let (Some(t), Some(pv)) = (&app.ui.transform, &app.transform_preview) else { return };
+
+    // Fix for ghosting after #9735: The async background commit leaves the transform preview populated.
+    // We must ensure the preview is only drawn if its document matches the currently active document.
+    if app.session.active().map(|st| st.doc.id) != Some(pv.doc.id) {
+        return;
+    }
+
     let scr = |q: [f64; 2]| xf.to_screen(q[0] as f32, q[1] as f32);
     if let Some(w) = &t.warp {
         let over_canvas = painter.ctx().input(|i| i.pointer.hover_pos()).filter(|p| xf.rect.contains(*p)).map(|p| xf.to_doc(p));
