diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs
--- a/crates/ui-egui/src/panels/properties.rs
+++ b/crates/ui-egui/src/panels/properties.rs
@@ -388,26 +388,30 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
             }
         });
         let fw = super::field_width(ui);
-        egui::Grid::new("ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
-            let r = ab.rect;
-            for (row, [(l1, k1, v1), (l2, k2, v2)]) in
-                [[("X:", "x", r.x0), ("W:", "width", r.width())], [("Y:", "y", r.y0), ("H:", "height", r.height())]].into_iter().enumerate()
+        let r = ab.rect;
+        // X and Y, then W and H, as two grids side by side: Tab goes down each column (#3953).
+        ui.horizontal_top(|ui| {
+            ui.spacing_mut().item_spacing.x = 4.0;
+            for (column, cells) in
+                [[("X:", "x", r.x0), ("Y:", "y", r.y0)], [("W:", "width", r.width()), ("H:", "height", r.height())]].into_iter().enumerate()
             {
-                for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
-                    dim_label(ui, label);
-                    let offset = match key {
-                        "x" => origin.x,
-                        "y" => origin.y,
-                        _ => 0.0,
-                    };
-                    if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v - offset), units, fw) {
-                        // A new X or Y moves the active artboard; a new W or H goes to every selected one.
-                        let indices = if matches!(key, "width" | "height") { super::artboards::resized_with(app, i) } else { vec![i] };
-                        let common = json!({"scaleArt": scale_art, "moveArt": move_art});
-                        super::artboards::set_props(app, &indices, common, |j, _| Some(json!({"index": j, key: v + offset}))).ok();
+                egui::Grid::new(("ab-grid", column)).num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                    for (row, (label, key, v)) in cells.into_iter().enumerate() {
+                        dim_label(ui, label);
+                        let offset = match key {
+                            "x" => origin.x,
+                            "y" => origin.y,
+                            _ => 0.0,
+                        };
+                        if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v - offset), units, fw) {
+                            // A new X or Y moves the active artboard; a new W or H goes to every selected one.
+                            let indices = if matches!(key, "width" | "height") { super::artboards::resized_with(app, i) } else { vec![i] };
+                            let common = json!({"scaleArt": scale_art, "moveArt": move_art});
+                            super::artboards::set_props(app, &indices, common, |j, _| Some(json!({"index": j, key: v + offset}))).ok();
+                        }
+                        ui.end_row();
                     }
-                }
-                ui.end_row();
+                });
             }
         });
         artboard_size_row(app, ui, i, ab.rect.width(), ab.rect.height());
@@ -698,25 +702,34 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
         }
         ui.add_space(6.0);
         let link = super::transform::proportional(app, ui);
-        egui::Grid::new("xf-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
-            dim_label(ui, "X:");
-            if let Some(v) = widgets::num_field(ui, "tx", Some(rp.x - origin.x), units, fw) {
-                app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi, "copy": copy})).ok();
-            }
-            dim_label(ui, "W:");
-            if let Some(v) = widgets::size_field(ui, "tw", Some(b.width()), units, fw) {
-                app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link, "copy": copy})).ok();
-            }
-            ui.end_row();
-            dim_label(ui, "Y:");
-            if let Some(v) = widgets::num_field(ui, "ty", Some(rp.y - origin.y), units, fw) {
-                app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi, "copy": copy})).ok();
-            }
-            dim_label(ui, "H:");
-            if let Some(v) = widgets::size_field(ui, "th", Some(b.height()), units, fw) {
-                app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link, "copy": copy})).ok();
-            }
-            ui.end_row();
+        // Two grids, X and Y then W and H: Tab follows the order the fields are made in, so it
+        // goes down each column (#3953).
+        ui.horizontal_top(|ui| {
+            ui.spacing_mut().item_spacing.x = 4.0;
+            egui::Grid::new("xf-grid-pos").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                dim_label(ui, "X:");
+                if let Some(v) = widgets::num_field(ui, "tx", Some(rp.x - origin.x), units, fw) {
+                    app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi, "copy": copy})).ok();
+                }
+                ui.end_row();
+                dim_label(ui, "Y:");
+                if let Some(v) = widgets::num_field(ui, "ty", Some(rp.y - origin.y), units, fw) {
+                    app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi, "copy": copy})).ok();
+                }
+                ui.end_row();
+            });
+            egui::Grid::new("xf-grid-size").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                dim_label(ui, "W:");
+                if let Some(v) = widgets::size_field(ui, "tw", Some(b.width()), units, fw) {
+                    app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link, "copy": copy})).ok();
+                }
+                ui.end_row();
+                dim_label(ui, "H:");
+                if let Some(v) = widgets::size_field(ui, "th", Some(b.height()), units, fw) {
+                    app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link, "copy": copy})).ok();
+                }
+                ui.end_row();
+            });
         });
         super::transform::constrain_link(app, ui);
     });
diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs
--- a/crates/ui-egui/src/panels/transform.rs
+++ b/crates/ui-egui/src/panels/transform.rs
@@ -94,25 +94,32 @@ fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect)
     ui.horizontal(|ui| {
         reference_locator(ui, refi);
         ui.add_space(4.0);
-        egui::Grid::new("xfp-ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
-            widgets::dim_label(ui, "X:");
-            if let Some(v) = widgets::num_field(ui, "xfp-ab-x", Some(rp.x - origin.x), units, 80.0) {
-                set(app, Some(v + origin.x), None, None, None);
-            }
-            widgets::dim_label(ui, "W:");
-            if let Some(v) = widgets::size_field(ui, "xfp-ab-w", Some(r.width()), units, 80.0) {
-                set(app, None, None, Some(v), None);
-            }
-            ui.end_row();
-            widgets::dim_label(ui, "Y:");
-            if let Some(v) = widgets::num_field(ui, "xfp-ab-y", Some(rp.y - origin.y), units, 80.0) {
-                set(app, None, Some(v + origin.y), None, None);
-            }
-            widgets::dim_label(ui, "H:");
-            if let Some(v) = widgets::size_field(ui, "xfp-ab-h", Some(r.height()), units, 80.0) {
-                set(app, None, None, None, Some(v));
-            }
-            ui.end_row();
+        ui.horizontal_top(|ui| {
+            ui.spacing_mut().item_spacing.x = 4.0;
+            egui::Grid::new("xfp-ab-grid-pos").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                widgets::dim_label(ui, "X:");
+                if let Some(v) = widgets::num_field(ui, "xfp-ab-x", Some(rp.x - origin.x), units, 80.0) {
+                    set(app, Some(v + origin.x), None, None, None);
+                }
+                ui.end_row();
+                widgets::dim_label(ui, "Y:");
+                if let Some(v) = widgets::num_field(ui, "xfp-ab-y", Some(rp.y - origin.y), units, 80.0) {
+                    set(app, None, Some(v + origin.y), None, None);
+                }
+                ui.end_row();
+            });
+            egui::Grid::new("xfp-ab-grid-size").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                widgets::dim_label(ui, "W:");
+                if let Some(v) = widgets::size_field(ui, "xfp-ab-w", Some(r.width()), units, 80.0) {
+                    set(app, None, None, Some(v), None);
+                }
+                ui.end_row();
+                widgets::dim_label(ui, "H:");
+                if let Some(v) = widgets::size_field(ui, "xfp-ab-h", Some(r.height()), units, 80.0) {
+                    set(app, None, None, None, Some(v));
+                }
+                ui.end_row();
+            });
         });
         constrain_link(app, ui);
     });
@@ -182,25 +189,32 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
         }
         ui.add_space(4.0);
         ui.add_enabled_ui(has, |ui| {
-            egui::Grid::new("xfp-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
-                widgets::dim_label(ui, "X:");
-                if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x - origin.x), units, 80.0) {
-                    app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi, "copy": copy})).ok();
-                }
-                widgets::dim_label(ui, "W:");
-                if let Some(v) = widgets::size_field(ui, "xfp-w", bounds.map(|b| b.width()), units, 80.0) {
-                    app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link, "copy": copy})).ok();
-                }
-                ui.end_row();
-                widgets::dim_label(ui, "Y:");
-                if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y - origin.y), units, 80.0) {
-                    app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi, "copy": copy})).ok();
-                }
-                widgets::dim_label(ui, "H:");
-                if let Some(v) = widgets::size_field(ui, "xfp-h", bounds.map(|b| b.height()), units, 80.0) {
-                    app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link, "copy": copy})).ok();
-                }
-                ui.end_row();
+            ui.horizontal_top(|ui| {
+                ui.spacing_mut().item_spacing.x = 4.0;
+                egui::Grid::new("xfp-grid-pos").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                    widgets::dim_label(ui, "X:");
+                    if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x - origin.x), units, 80.0) {
+                        app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi, "copy": copy})).ok();
+                    }
+                    ui.end_row();
+                    widgets::dim_label(ui, "Y:");
+                    if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y - origin.y), units, 80.0) {
+                        app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi, "copy": copy})).ok();
+                    }
+                    ui.end_row();
+                });
+                egui::Grid::new("xfp-grid-size").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
+                    widgets::dim_label(ui, "W:");
+                    if let Some(v) = widgets::size_field(ui, "xfp-w", bounds.map(|b| b.width()), units, 80.0) {
+                        app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link, "copy": copy})).ok();
+                    }
+                    ui.end_row();
+                    widgets::dim_label(ui, "H:");
+                    if let Some(v) = widgets::size_field(ui, "xfp-h", bounds.map(|b| b.height()), units, 80.0) {
+                        app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link, "copy": copy})).ok();
+                    }
+                    ui.end_row();
+                });
             });
         });
         constrain_link(app, ui);
@@ -617,6 +631,60 @@ mod tests {
         assert_eq!(artboard_rect(r, 8, None, Some(500.0), None, None, false), Rect::new(0.0, 300.0, 300.0, 500.0));
     }
 
+    /// Where each Tab stop of `draw` is on screen, in the order Tab visits them.
+    fn tab_stops(app: &mut VectorcraftApp, draw: fn(&mut VectorcraftApp, &mut Ui)) -> Vec<egui::Rect> {
+        let ctx = egui::Context::default();
+        crate::theme::install_fonts(&ctx);
+        let mut time = 0.0;
+        let mut frame = |app: &mut VectorcraftApp, tab: bool| {
+            time += 0.05;
+            let events = if tab {
+                vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }]
+            } else {
+                vec![]
+            };
+            let screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 400.0)));
+            let raw = egui::RawInput { events, time: Some(time), screen_rect, focused: true, ..Default::default() };
+            let mut out = ctx.run_ui(raw, |ui| draw(app, ui));
+            out.textures_delta.clear();
+        };
+        // The grids lay themselves out on their second frame.
+        frame(app, false);
+        frame(app, false);
+        let mut stops = vec![];
+        for _ in 0..20 {
+            frame(app, true);
+            frame(app, false);
+            if let Some(r) = ctx.memory(|m| m.focused()).and_then(|id| ctx.read_response(id)) {
+                stops.push(r.rect);
+            }
+        }
+        stops
+    }
+
+    /// #3953: Tab goes X, Y, W, H (down the first column, then down the second), while the fields
+    /// stay where they were: X and W on the first row, Y and H on the second.
+    #[test]
+    fn tab_goes_x_y_w_h() {
+        let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
+        app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
+        app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 30})).unwrap();
+        check_xywh_order("Transform", &mut app, show, 0);
+        check_xywh_order("Properties", &mut app, crate::panels::properties::transform_section, 0);
+        app.run("tool.select", json!({"tool": "artboard"})).unwrap();
+        check_xywh_order("Artboard", &mut app, show, 0);
+        // The Properties panel starts with the artboard's name field.
+        check_xywh_order("Edit Artboards", &mut app, crate::panels::properties::show, 1);
+    }
+
+    fn check_xywh_order(name: &str, app: &mut VectorcraftApp, draw: fn(&mut VectorcraftApp, &mut Ui), skip: usize) {
+        // The number fields are the wide stops; the reference point's and the scrub labels' are small.
+        let fields: Vec<egui::Rect> = tab_stops(app, draw).into_iter().filter(|r| r.width() > 50.0).skip(skip).take(4).collect();
+        let [x, y, w, h] = fields[..] else { panic!("{name}: four fields expected, got {fields:?}") };
+        assert!(x.min.x == y.min.x && w.min.x == h.min.x && x.max.x < w.min.x, "{name}: columns {fields:?}");
+        assert!(x.min.y == w.min.y && y.min.y == h.min.y && x.max.y < y.min.y, "{name}: rows {fields:?}");
+    }
+
     #[test]
     fn constrain_proportions() {
         assert_eq!(constrained(100.0, 50.0, Some(200.0), None), (200.0, 100.0));
