diff --git a/crates/ui-egui/src/panels/options_bar.rs b/crates/ui-egui/src/panels/options_bar.rs
--- a/crates/ui-egui/src/panels/options_bar.rs
+++ b/crates/ui-egui/src/panels/options_bar.rs
@@ -534,6 +534,11 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
                                     if icons::button(ui, "ban", 26.0, false, tl!("Cancel current crop operation  (Esc)")).clicked() {
                                         crate::crop_ui::cancel(app);
                                     }
+                                    // Resets the box to its first frame; nothing to reset until it is changed.
+                                    let changed = crate::crop_ui::modified(app);
+                                    if ui.add_enabled_ui(changed, |ui| icons::button(ui, "undo-2", 26.0, false, tl!("Reset"))).inner.clicked() {
+                                        crate::crop_ui::reset(app);
+                                    }
                                 });
                             }
                             // Every theme gets the font, style and size controls (#1387), as with Auto-Select (#1275).
diff --git a/crates/ui-egui/src/tools/crop_ui.rs b/crates/ui-egui/src/tools/crop_ui.rs
--- a/crates/ui-egui/src/tools/crop_ui.rs
+++ b/crates/ui-egui/src/tools/crop_ui.rs
@@ -347,6 +347,15 @@ pub fn ensure_frame(app: &mut PhotocraftApp) {
     crate::crop_mode::sync(app, None);
 }
 
+/// The frame a new crop starts from: the selection's bounds, or the whole canvas.
+fn default_rect(app: &PhotocraftApp) -> Option<[f64; 4]> {
+    let st = app.session.active()?;
+    let canvas = st.doc.bounds();
+    // Computed once per new frame, not per frame drawn.
+    let r = st.doc.selection.as_ref().map(|s| s.content_bounds().intersect(&canvas)).filter(|b| !b.is_empty()).unwrap_or(canvas);
+    (!r.is_empty()).then(|| [f64::from(r.x0), f64::from(r.y0), f64::from(r.x1), f64::from(r.y1)])
+}
+
 fn frame(app: &mut PhotocraftApp) {
     if app.ui.tool != Tool::Crop {
         app.crop.editing = false;
@@ -377,14 +386,8 @@ fn frame(app: &mut PhotocraftApp) {
     if app.ui.crop_rect.is_some() || app.crop.drag.is_some() {
         return;
     }
-    let Some(st) = app.session.active() else { return };
-    let canvas = st.doc.bounds();
-    // Computed once per new frame, not per frame drawn.
-    let r = st.doc.selection.as_ref().map(|s| s.content_bounds().intersect(&canvas)).filter(|b| !b.is_empty()).unwrap_or(canvas);
-    if r.is_empty() {
-        return;
-    }
-    app.ui.crop_rect = Some([f64::from(r.x0), f64::from(r.y0), f64::from(r.x1), f64::from(r.y1)]);
+    let Some(r) = default_rect(app) else { return };
+    app.ui.crop_rect = Some(r);
     app.crop.default_frame = true;
     app.crop.frame_for = key;
     app.crop.editing = false;
@@ -433,6 +436,37 @@ pub fn cancel(app: &mut PhotocraftApp) {
     crate::crop_straighten::reset(app);
 }
 
+/// The frame was drawn, moved, resized or turned since it was last framed by default, so
+/// [`reset`] has something to undo.
+pub fn modified(app: &PhotocraftApp) -> bool {
+    app.ui.tool == Tool::Crop && app.ui.crop_rect.is_some() && app.crop.drag.is_none() && (!app.crop.default_frame || app.ui.crop_angle != 0.0)
+}
+
+/// Reset the crop box (the options bar's Reset): the default frame again, upright, with the
+/// straighten line gone. The tool stays up and the old frame is a box Undo step, unlike
+/// [`cancel`], which drops the box's history.
+pub fn reset(app: &mut PhotocraftApp) {
+    if !modified(app) {
+        return;
+    }
+    let Some(rect) = default_rect(app) else { return };
+    let before = step_of(app);
+    app.ui.crop_rect = Some(rect);
+    app.ui.crop_angle = 0.0;
+    app.crop.default_frame = true;
+    app.crop.frame_for = app.session.active_index().zip(app.session.active()).map(|(i, st)| (i, st.doc.size.width, st.doc.size.height));
+    app.crop.editing = false;
+    app.crop.press_step = None;
+    claim(app);
+    crate::crop_straighten::reset(app);
+    if step_of(app) != before {
+        app.crop.undo.push(before);
+        app.crop.redo.clear();
+    }
+    crate::crop_mode::sync_frame(app);
+    crate::crop_mode::auto_center(app);
+}
+
 /// Drop the box's own Undo/Redo (leaving the tool, cancelling or committing a crop).
 pub fn clear_history(app: &mut PhotocraftApp) {
     app.crop.undo.clear();
diff --git a/crates/ui-egui/src/tools/crop_ui/tests.rs b/crates/ui-egui/src/tools/crop_ui/tests.rs
--- a/crates/ui-egui/src/tools/crop_ui/tests.rs
+++ b/crates/ui-egui/src/tools/crop_ui/tests.rs
@@ -109,6 +109,72 @@ fn picking_the_tool_frames_the_canvas_or_the_selection() {
     assert_eq!(app.ui.crop_rect, None);
 }
 
+/// #9730: the options bar's Reset puts the box back to its first frame, as an Undo step.
+#[test]
+fn reset_frames_the_canvas_again_and_can_be_undone() {
+    let mut app = app(SampleType::U8);
+    ensure_frame(&mut app);
+    assert!(!modified(&app), "the untouched default frame has nothing to reset");
+    reset(&mut app);
+    assert!(app.crop.undo.is_empty(), "so resetting it adds no step");
+
+    drag(&mut app, &[[50.0, 20.0], [80.0, 40.0], [120.0, 70.0]], NONE);
+    assert_eq!(app.ui.crop_rect, Some([50.0, 20.0, 120.0, 70.0]));
+    assert!(modified(&app));
+    reset(&mut app);
+    assert_eq!(app.ui.crop_rect, Some([0.0, 0.0, 200.0, 100.0]));
+    assert!(!modified(&app) && app.crop.default_frame);
+    assert_eq!(app.session.active().unwrap().doc.size, Size::new(200, 100), "nothing is cropped");
+
+    // The box's own Undo brings the drawn frame back, and Redo resets it again.
+    assert!(intercept(&mut app, "edit.undo").is_some());
+    assert_eq!(app.ui.crop_rect, Some([50.0, 20.0, 120.0, 70.0]));
+    assert!(intercept(&mut app, "edit.redo").is_some());
+    assert_eq!(app.ui.crop_rect, Some([0.0, 0.0, 200.0, 100.0]));
+
+    // A turned default frame is a changed one too: Reset makes it upright.
+    app.ui.crop_angle = 0.3;
+    assert!(modified(&app));
+    reset(&mut app);
+    assert_eq!((app.ui.crop_angle, modified(&app)), (0.0, false));
+
+    // With a selection, the first frame is its bounds.
+    drag(&mut app, &[[50.0, 20.0], [80.0, 40.0], [120.0, 70.0]], NONE);
+    app.run("select.rect", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
+    reset(&mut app);
+    assert_eq!(app.ui.crop_rect, Some([10.0, 10.0, 40.0, 30.0]));
+
+    // Another tool has no box to reset.
+    drag(&mut app, &[[60.0, 30.0], [100.0, 60.0]], NONE);
+    app.ui.tool = Tool::Brush;
+    assert!(!modified(&app));
+}
+
+/// #9730: the Crop tool's options bar has a Reset button beside Cancel and Commit, live once
+/// the frame was changed.
+#[test]
+fn the_options_bar_reset_button_resets_a_changed_frame() {
+    use egui_kittest::kittest::{NodeT, Queryable};
+    let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
+    app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
+    let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_max_steps(64).build_eframe(move |cc| {
+        PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
+        app
+    });
+    h.state_mut().ui.tool = Tool::Crop;
+    h.run_steps(8);
+    let button = |h: &egui_kittest::Harness<'_, PhotocraftApp>| h.get_by_role_and_label(egui::accesskit::Role::Button, "Reset").accesskit_node().is_disabled();
+    assert!(button(&h), "an untouched frame has nothing to reset");
+    h.state_mut().ui.crop_rect = Some([50.0, 20.0, 120.0, 70.0]);
+    h.state_mut().crop.default_frame = false;
+    h.run_steps(4);
+    assert!(!button(&h), "a changed frame can be reset");
+    h.get_by_role_and_label(egui::accesskit::Role::Button, "Reset").click();
+    h.run_steps(4);
+    assert_eq!(h.state().ui.crop_rect, Some([0.0, 0.0, 200.0, 100.0]));
+    assert!(button(&h));
+}
+
 #[test]
 fn enter_on_the_selection_frame_crops_to_it_at_8_and_16_bit() {
     // #1789: selection, C, ↵ did nothing, as the untouched frame was always treated as a no-op.
