diff --git a/crates/ui-egui/src/tools/comments/input.rs b/crates/ui-egui/src/tools/comments/input.rs
--- a/crates/ui-egui/src/tools/comments/input.rs
+++ b/crates/ui-egui/src/tools/comments/input.rs
@@ -12,6 +12,7 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
     let (pointer, origin) = (cx.ptr.pos, cx.ptr.origin);
     let page_rect = cx.xf.rect;
     let pressed_here = origin.is_some_and(|o| page_rect.contains(o));
+    let handle_pressed = origin.is_some_and(|o| page_rect.expand(12.0).contains(o));
     let over_page = pointer.is_some_and(|p| page_rect.contains(p));
     let fill_grab = fill_grabs(cx, view);
     let draw_grab = draw_grabs(cx, view);
@@ -34,7 +35,7 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
     match cx.tool {
         // The shape just drawn is selected with its handles showing: they resize it without a
         // trip to the Select tool (#2338). Anywhere else the tool keeps drawing.
-        QuickTool::Comment(_) if draw_grab => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
+        QuickTool::Comment(_) if draw_grab => select_input(ui, resp, cx, view, pointer, origin, handle_pressed),
         QuickTool::Comment(tool) if tool.draws() => {
             if !cx.allowed {
                 return false;
@@ -272,8 +273,8 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
             }
             true
         }
-        QuickTool::Select => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
-        QuickTool::Fill(_) if fill_grab => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
+        QuickTool::Select => select_input(ui, resp, cx, view, pointer, origin, handle_pressed),
+        QuickTool::Fill(_) if fill_grab => select_input(ui, resp, cx, view, pointer, origin, handle_pressed),
         _ => false,
     }
 }
@@ -327,9 +328,9 @@ fn grabs(cx: &PageCx<'_>, view: &DocView, pick: impl Fn(&Annotation) -> bool) ->
 }
 
 /// Where a gesture on this page is decided: the press while the button is down, otherwise the
-/// pointer (egui clears the press origin on release). `None` off the page.
+/// pointer (egui clears the press origin on release). `None` off the page or beyond handle reach.
 fn press_point(cx: &PageCx<'_>) -> Option<Pos2> {
-    (if cx.ptr.any_down { cx.ptr.origin } else { cx.ptr.pos }).filter(|p| cx.xf.rect.contains(*p))
+    (if cx.ptr.any_down { cx.ptr.origin } else { cx.ptr.pos }).filter(|p| cx.xf.rect.expand(12.0).contains(*p))
 }
 
 /// Whether `p` is on a resize handle of the selected comment, when it is a resizable `pick`.
@@ -446,7 +447,7 @@ fn select_input(
         }
     }
     // (egui clears the press origin on release, so clicks are located by the pointer.)
-    let on_page = pointer.is_some_and(|p| cx.xf.rect.contains(p));
+    let on_page = pointer.is_some_and(|p| cx.xf.rect.expand(12.0).contains(p));
     if resp.clicked() && on_page {
         let follow_link = cx.tool == QuickTool::Select
             && !consumed
@@ -454,7 +455,8 @@ fn select_input(
             && !resp.double_clicked()
             && pointer
                 .is_some_and(|p| cx.info.links.iter().any(|link| link.page == cx.page && cx.xf.user_rect(cx.info, cx.page, link.rect).contains(p)));
-        match pointer.and_then(|p| cx.hit(p)) {
+        let handle_hit = selected.filter(|_| pointer.is_some_and(|p| handle_at(p).is_some() || point_at(p).is_some()));
+        match handle_hit.or_else(|| pointer.and_then(|p| cx.hit(p))) {
             Some(a) if a.subtype == "Highlight" && follow_link => {
                 // A plain click follows the link beneath text highlighting. Keep other
                 // annotations and active gestures in control of their own input.
diff --git a/crates/ui-egui/tests/it/comments.rs b/crates/ui-egui/tests/it/comments.rs
--- a/crates/ui-egui/tests/it/comments.rs
+++ b/crates/ui-egui/tests/it/comments.rs
@@ -2657,3 +2657,71 @@ fn text_comments_can_save_a_zero_width_default() {
     assert_eq!(restored.comment_prefs.style(CommentTool::TextBox).width, 0.0);
     assert_eq!(restored.comment_prefs.style(CommentTool::Callout).width, 0.0);
 }
+
+/// #4825: A comment or text box placed near the edge of a page can have its resize handles
+/// extend slightly outside the page boundary. Dragging an off-page handle must be recognized,
+/// entering `Gesture::Resize` and updating the comment's size, while starting a new drawing off-page remains blocked.
+#[test]
+fn off_page_resize_handle_is_selectable_and_resizes() {
+    use pdfcraft_engine::{Edit, NewAnnotation, Shape};
+    use pdfcraft_ui_egui::comments::Gesture;
+    let mut h = harness(|app| app.set_option("quick", "select").unwrap());
+    let shape = Shape::TextBox { rect: [0.0, 50.0, 100.0, 150.0], font_size: 12.0 };
+    let style = pdfcraft_engine::Style::default_for(&shape);
+    h.state_mut().apply_edit(Edit::AddAnnotation(NewAnnotation { page: 0, shape, style, contents: "Edge box".into(), author: "Tester".into() }));
+    h.run_steps(2);
+    let c = comments(&h);
+    // Select the comment so its resize handles appear.
+    let idx = c[0].index;
+    h.state_mut().views[0].comments.selected = Some((0, idx));
+    h.run_steps(2);
+
+    let page_rect = h.state().views[0].page_screen_rect(0).expect("page 1 on screen");
+    // The comment's left edge is at x = 0.0, which corresponds to page_rect.left().
+    // The left-middle handle (-1, 0) is centered at x = page_rect.left().
+    // Position the pointer 3 pixels to the left of the page (outside page_rect, but on the handle).
+    let handle_y = page_rect.top() + 0.5 * page_rect.height();
+    let off_page_from = pos2(page_rect.left() - 3.0, handle_y);
+    assert!(!page_rect.contains(off_page_from), "start point is outside the page bounds");
+
+    // Begin dragging the off-page handle to the right.
+    let to = pos2(page_rect.left() + 30.0, handle_y);
+    h.hover_at(off_page_from);
+    h.run_steps(1);
+    h.drag_at(off_page_from);
+    h.run_steps(1);
+    h.hover_at(off_page_from + (to - off_page_from) * 0.25);
+    h.run_steps(1);
+
+    // Verify that the resize gesture is actively recognized even though the press was off-page.
+    assert!(matches!(h.state().views[0].comments.gesture, Some(Gesture::Resize { .. })), "dragging an off-page handle starts a resize gesture");
+
+    for k in 2..=4 {
+        h.hover_at(off_page_from + (to - off_page_from) * (k as f32 / 4.0));
+        h.run_steps(1);
+    }
+    h.drop_at(to);
+    h.run_steps(3);
+
+    // The comment must have been resized and registered in undo history.
+    let c = comments(&h);
+    assert_eq!(c.len(), 1);
+    let after_rect = c[0].rect;
+    assert!(after_rect[0] > 0.0, "left edge moved inward: {after_rect:?}");
+    assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Resize comment"));
+
+    // Verify that drawing a new shape starting off-page does not begin a drag or create a comment.
+    let mut h_draw = harness(|app| app.set_option("quick", "square").unwrap());
+    let page_rect_draw = h_draw.state().views[0].page_screen_rect(0).expect("page 1 on screen");
+    let off_page_draw = pos2(page_rect_draw.left() - 4.0, page_rect_draw.top() + 50.0);
+    h_draw.hover_at(off_page_draw);
+    h_draw.run_steps(1);
+    h_draw.drag_at(off_page_draw);
+    h_draw.run_steps(1);
+    h_draw.hover_at(pos2(page_rect_draw.left() + 40.0, page_rect_draw.top() + 50.0));
+    h_draw.run_steps(1);
+    assert!(h_draw.state().views[0].comments.gesture.is_none(), "off-page drag does not start a new drawing");
+    h_draw.drop_at(pos2(page_rect_draw.left() + 40.0, page_rect_draw.top() + 50.0));
+    h_draw.run_steps(2);
+    assert_eq!(comments(&h_draw).len(), 0, "no comment created from off-page press");
+}
