diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index 07a857749594c6722cde64de2092bf787ecd773b..8308b66de68e7e468aaca4a9b2c98675cfdc218c 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -415,9 +415,16 @@ select {
             </select>
             <br/>
             <label data-i18n="[title]prefs.gui.grid.show_grid_tt">
-              <input type="checkbox" id="show_grid" name="show_grid">
+              <input type="checkbox" id="show_grid" name="show_grid"
+                     onchange="grid_toggle(this.checked)">
               <span data-i18n="prefs.gui.grid.show_grid"></span>
             </label>
+	    <select id="grid_size">
+              <option value="5">5</option>
+              <option value="10">10</option>
+              <option value="20">20</option>
+              <option value="25">25</option>
+            </select>
             <br/>
             <label data-i18n="[title]prefs.gui.zoom.save_zoom_tt">
               <input type="checkbox" id="save_zoom" name="save_zoom">
@@ -783,6 +790,7 @@ function apply(save_prefs) {
     pdgui.pdsend("pd gui-prefs",
         get_gui_preset(),
         get_bool_elem("show_grid"),
+	document.getElementById("grid_size").value,
         get_bool_elem("save_zoom"),
         get_bool_elem("browser_doc"),
         get_bool_elem("browser_path"),
@@ -796,7 +804,8 @@ function apply(save_prefs) {
         get_bool_elem("browser_path")
     );
     // Update the grid on all open windows.
-    pdgui.update_grid(get_bool_elem("show_grid"));
+    pdgui.update_grid(get_bool_elem("show_grid"),
+        document.getElementById("grid_size").value);
 
     // Send the startup config data to Pd
     pdgui.pdsend.apply(null, ["pd path-dialog", startup_use_stdpath, startup_verbose].concat(get_path_array()));
@@ -1035,13 +1044,18 @@ function midi_prefs_callback(attrs) {
     pdgui.resize_window(pd_object_callback);
 }
 
+function grid_toggle(checked) {
+    document.getElementById("grid_size").disabled = !checked;
+    document.getElementById("show_grid").checked = checked;
+}
+
 function autopatch_yoffset_toggle(checked) {
     document.getElementById("autopatch_yoffset_value").disabled = !checked;
     document.getElementById("autopatch_yoffset").checked = checked;
 }
 
-function gui_prefs_callback(name, show_grid, save_zoom, browser_doc, browser_path,
-    browser_init, autopatch_yoffset) {
+function gui_prefs_callback(name, show_grid, grid_size, save_zoom, browser_doc,
+    browser_path, browser_init, autopatch_yoffset) {
     var s = document.getElementById("gui_preset");
 
     // ag: scan the css subdir for user-defined styles
@@ -1071,6 +1085,8 @@ function gui_prefs_callback(name, show_grid, save_zoom, browser_doc, browser_pat
         }
     }
     document.getElementById("show_grid").checked = !!show_grid;
+    document.getElementById("grid_size").value = grid_size;
+    grid_toggle(!!show_grid);
     document.getElementById("save_zoom").checked = !!save_zoom;
     document.getElementById("browser_doc").checked = !!browser_doc;
     document.getElementById("browser_path").checked = !!browser_path;
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index 64e80ab85453a7bca051f3509c3a068ff54e36fd..dad6f9f51163ce1043e1b59f17956ae5b47b86eb 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -1405,36 +1405,41 @@ function menu_send(name) {
     }
 }
 
-/*
-ico@vt.edu 20200907: added svg tiled background to reflect edit mode and
-integrated it into the canvas_set_editmode below.
-
-LATER: consider adding an interim version that reflects only the ctrl button press
-*/
-var gui_editmode_svg_background = "url(\"data:image/svg+xml,%3Csvg " +
-        "xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 " +
-        " 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity" +
-        "='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1" +
-        "v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-" +
-        "1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9" +
-        "H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9" +
-        "V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v" +
-        "1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h" +
-        "9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-1" +
-        "0 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9" +
-        "h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-" +
-        "10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9" +
-        "h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9" +
-        "v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h" +
-        "9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h" +
-        "9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-1" +
-        "0 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-" +
-        "9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm1" +
-        "0 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9" +
-        "h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9" +
-        "h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0" +
-        "h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d" +
-        "='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")";
+// Background for edit mode. Currently, we use a grid if snap-to-grid
+// functionality is turned on in the GUI preferences. If not, we just use
+// the same grid with a lower opacity. That way the edit mode is always
+// visually distinct from run mode.
+var create_editmode_background = function(grid, size) {
+    var head, body, tail, cell_data_str, opacity_str;
+    // if snap-to-grid isn't turned on, just use cell size of 10 and make the
+    // grid partially transparent
+    size = grid ? size : 10;
+    opacity_str = '"' + (grid ? 1 : 0.25) + '"';
+post("size is " + size);
+    cell_data_str = ['"', "M", size, 0, "L", 0, 0, 0, size, '"'].join(" ");
+
+    head = ['<svg xmlns="http://www.w3.org/2000/svg" ',
+                'width="100%" height="100%" ',
+                'opacity=', opacity_str, '>']
+           .join("");
+    body = ['<defs>',
+              '<pattern id="cell" patternUnits="userSpaceOnUse" ',
+                       'width="', size, '" height="', size, '">',
+                '<path fill="none" stroke="#ddd" stroke-width="1" ',
+                      'd=', cell_data_str,'/>',
+              '</pattern>',
+              '<pattern id="grid" patternUnits="userSpaceOnUse" ',
+                        'width="100" height="100">',
+                '<rect width="100" height="100" fill="url(#cell)" />',
+                '<path fill="none" stroke="#bbb" stroke-width="1" ',
+                      'd="M 100 0 L 0 0 0 100"/>',
+              '</pattern>',
+            '</defs>',
+            '<rect width="100%" height="100%" fill="url(#grid)" />'
+        ].join("");
+    tail = '</svg>';
+    return "url('data:image/svg+xml;utf8," + head + body + tail + "')";
+}
 
 // requires nw.js API (Menuitem)
 function canvas_set_editmode(cid, state) {
@@ -1442,15 +1447,17 @@ function canvas_set_editmode(cid, state) {
         w.set_editmode_checkbox(state !== 0 ? true : false);
         if (state !== 0) {
             patchsvg.classList.add("editmode");
-            if (showgrid[cid]) {
-                //post("editmode:" + gui_editmode_svg_background);
-                patchwin[cid].window.document.body.style.setProperty
-                ("background-image", gui_editmode_svg_background);
-            }
+            // For now, we just change the opacity of the background grid
+            // depending on whether snap-to-grid is turned on. This way
+            // edit mode is always visually distinct.
+            patchwin[cid].window.document.body.style
+	        .setProperty("background-image",
+                    create_editmode_background(showgrid[cid], gridsize[cid]));
         } else {
             patchsvg.classList.remove("editmode");
-            patchwin[cid].window.document.body.style.setProperty("background-image",
-                "none");  
+            patchwin[cid].window.document.body.style
+                .setProperty("background-image", "none"
+            );
         }
     });
 }
@@ -1468,21 +1475,21 @@ function canvas_query_editmode(cid) {
 
 exports.canvas_query_editmode = canvas_query_editmode;
 
-function update_grid(grid) {
+function update_grid(grid, grid_size_value) {
     // Update the grid background of all canvas windows when the corresponding
     // option in the gui prefs changes.
-    var bg = grid != 0 ? gui_editmode_svg_background : "none";
     for (var cid in patchwin) {
 	gui(cid).get_elem("patchsvg", function(patchsvg, w) {
             var editmode = patchsvg.classList.contains("editmode");
             if (editmode) {
                 patchwin[cid].window.document.body.style.setProperty
-                ("background-image", bg);
+                ("background-image", create_editmode_background(grid !== 0,
+                    grid_size_value));
             }
 	});
     }
     // Also update the showgrid flags.
-    set_showgrid(grid);
+    set_grid(grid, grid_size_value);
 }
 
 exports.update_grid = update_grid;
@@ -1744,6 +1751,7 @@ var scroll = {},
     font = {},
     doscroll = {},
     showgrid = {},
+    gridsize = {},
     last_loaded, // last loaded canvas
     last_focused, // last focused canvas (doesn't include Pd window or dialogs)
     loading = {},
@@ -1754,9 +1762,11 @@ var scroll = {},
     var patchwin = {}; // object filled with cid: [Window object] pairs
     var dialogwin = {}; // object filled with did: [Window object] pairs
 
-var set_showgrid = function(grid) {
-    for (var cid in showgrid) {
+var set_grid = function(grid, gridsize_value) {
+    var cid;
+    for (cid in showgrid) {
 	showgrid[cid] = grid;
+        gridsize[cid] = gridsize_value;
     }
 }
 
@@ -1919,7 +1929,9 @@ function create_window(cid, type, width, height, xpos, ypos, attr_array) {
 }
 
 // create a new canvas
-function gui_canvas_new(cid, width, height, geometry, grid, zoom, editmode, name, dir, dirty_flag, warid, hide_scroll, hide_menu, has_toplevel_scalars, cargs) {
+function gui_canvas_new(cid, width, height, geometry, grid, grid_size_value,
+    zoom, editmode, name, dir, dirty_flag, warid, hide_scroll, hide_menu,
+    has_toplevel_scalars, cargs) {
     // hack for buggy tcl popups... should go away for node-webkit
     //reset_ctrl_on_popup_window
     
@@ -1947,6 +1959,7 @@ function gui_canvas_new(cid, width, height, geometry, grid, zoom, editmode, name
     font[cid] = 10;
     doscroll[cid] = 0;
     showgrid[cid] = grid != 0;
+    gridsize[cid] = grid_size_value;
     toplevel_scalars[cid] = has_toplevel_scalars;
     // geometry is just the x/y screen offset "+xoff+yoff"
     geometry = geometry.slice(1);   // remove the leading "+"
@@ -6190,10 +6203,10 @@ function gui_midi_properties(gfxstub, sys_indevs, sys_outdevs,
     }
 }
 
-function gui_gui_properties(dummy, name, show_grid, save_zoom, browser_doc, browser_path,
+function gui_gui_properties(dummy, name, show_grid, grid_size, save_zoom, browser_doc, browser_path,
     browser_init, autopatch_yoffset) {
     if (dialogwin["prefs"] !== null) {
-        dialogwin["prefs"].window.gui_prefs_callback(name, show_grid, save_zoom,
+        dialogwin["prefs"].window.gui_prefs_callback(name, show_grid, grid_size, save_zoom,
             browser_doc, browser_path, browser_init, autopatch_yoffset);
     }
 }
diff --git a/pd/src/g_editor.c b/pd/src/g_editor.c
index ef71817264cea7a3ee78a7b194252418b37c434b..1eee2dcdf168840e40df6719a6393f20401717b1 100644
--- a/pd/src/g_editor.c
+++ b/pd/src/g_editor.c
@@ -2799,6 +2799,9 @@ void canvas_init_menu(t_canvas *x)
     gui_vmess("gui_menu_font_set_initial_size", "xi", x, x->gl_font);
 }
 
+extern int sys_snaptogrid; /* whether we are snapping to grid or not */
+extern int sys_gridsize;
+
 void canvas_vis(t_canvas *x, t_floatarg f)
 {
     //fprintf(stderr,"canvas_vis .x%zx %f\n", (t_int)x, f);
@@ -2809,7 +2812,6 @@ void canvas_vis(t_canvas *x, t_floatarg f)
 
     t_gobj *g;
     t_int properties;
-    extern int sys_grid;
 
     int flag = (f != 0);
     if (flag)
@@ -2865,12 +2867,13 @@ void canvas_vis(t_canvas *x, t_floatarg f)
                We may need to expand this to include scalars, as well. */
             canvas_create_editor(x);
             canvas_args_to_string(argsbuf, x);
-            gui_vmess("gui_canvas_new", "xiisiiissiiiiis",
+            gui_vmess("gui_canvas_new", "xiisiiiissiiiiis",
                 x,
                 (int)(x->gl_screenx2 - x->gl_screenx1),
                 (int)(x->gl_screeny2 - x->gl_screeny1),
                 geobuf,
-                sys_grid,
+                sys_snaptogrid,
+		sys_gridsize,
                 x->gl_zoom,
                 x->gl_edit,
                 x->gl_name->s_name,
@@ -3637,6 +3640,8 @@ static int canvas_upx, canvas_upy;
 
 static int ctrl_runmode_warned;
 
+static int snap_got_anchor;
+
     /* mouse click */
 void canvas_doclick(t_canvas *x, int xpos, int ypos, int which,
     int mod, int doit)
@@ -3646,6 +3651,10 @@ void canvas_doclick(t_canvas *x, int xpos, int ypos, int which,
        to array_motion so that we can update corresponding send when
        the array has been changed */
     array_garray = NULL;
+
+    /* reset the snap_got_anchor variable so the the snap_to_grid feature
+       can find its anchor object before it starts to displace a selection */
+    snap_got_anchor = 0;
     //post("canvas_doclick %d", doit);
 
     t_gobj *y;
@@ -5767,15 +5776,78 @@ void canvas_key(t_canvas *x, t_symbol *s, int ac, t_atom *av)
 extern void graph_checkgop_rect(t_gobj *z, t_glist *glist,
     int *xp1, int *yp1, int *xp2, int *yp2);
 
+    /* We get the bbox for the object under the mouse the first time around,
+       then cache its offset from the mouse for future motion messages.
+       Since there are cases where snapping to a grid can move the object
+       relative to the mouse pointer, we can't rely on our "anchor" object to
+       always be directly under the mouse coordinates. */
+static void snap_get_anchor_xy(t_canvas *x, int *gobj_x, int *gobj_y)
+{
+    t_selection *s = x->gl_editor->e_selection;
+    int x1, y1, x2, y2;
+    while (s)
+    {
+        if (canvas_hitbox(x, s->sel_what, x->gl_editor->e_xwas,
+            x->gl_editor->e_ywas, &x1, &y1, &x2, &y2))
+        {
+            *gobj_x = x1;
+            *gobj_y = y1;
+            return;
+        }
+	s = s->sel_next;
+    }
+    bug("canvas_get_snap_offset");
+}
+
+int anchor_xoff;
+int anchor_yoff;
+
+static void canvas_snap_to_grid(t_canvas *x, int xwas, int ywas, int xnew,
+    int ynew, int *dx, int *dy)
+{
+    int gsize = sys_gridsize;
+        /* If we're snapping to grid, we need an initial delta to align
+           the object under the mouse to the given gridlines. We keep
+           that in the variables below, which will have no affect after
+           our initial grid adjustment. */
+    int snap_dx = 0, snap_dy = 0;
+    if (!snap_got_anchor)
+    {
+        int obx = xnew, oby = ynew;
+        snap_get_anchor_xy(x, &obx, &oby);
+            /* First, get the distance the selection should be displaced
+               in order to align the anchor object with a grid line. */
 
+        snap_dx = floor((obx + (gsize / 2)) / gsize) * gsize - obx;
+
+        snap_dy = floor((oby + (gsize / 2)) / gsize) * gsize - oby;
+        obx = floor(obx / gsize) * gsize;
+        oby = floor(oby / gsize) * gsize;
+        anchor_xoff = xnew - obx;
+        anchor_yoff = ynew - oby;
+        snap_got_anchor = 1;
+    }
+    *dx = floor((xnew - anchor_xoff) / gsize) * gsize -
+        floor((xwas - anchor_xoff) / gsize) * gsize + snap_dx;
+    *dy = floor((ynew - anchor_yoff) / gsize) * gsize -
+        floor((ywas - anchor_yoff) / gsize) * gsize + snap_dy;
+}
 
 static void delay_move(t_canvas *x)
 {
-    canvas_displaceselection(x,
-        x->gl_editor->e_xnew - x->gl_editor->e_xwas,
-        x->gl_editor->e_ynew - x->gl_editor->e_ywas);
-    x->gl_editor->e_xwas = x->gl_editor->e_xnew;
-    x->gl_editor->e_ywas = x->gl_editor->e_ynew;
+    int dx, dy;
+    int xwas = x->gl_editor->e_xwas, ywas = x->gl_editor->e_ywas,
+        xnew = x->gl_editor->e_xnew, ynew = x->gl_editor->e_ynew;
+    if (sys_snaptogrid)
+        canvas_snap_to_grid(x, xwas, ywas, xnew, ynew, &dx, &dy);
+    else
+    {
+        dx = xnew - xwas;
+        dy = ynew - ywas;
+    }
+    canvas_displaceselection(x, dx, dy);
+    x->gl_editor->e_xwas = xnew;
+    x->gl_editor->e_ywas = ynew;
 }
 
 void canvas_motion(t_canvas *x, t_floatarg xpos, t_floatarg ypos,
diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c
index 2147047271868a021bb4f899b9aefdeecf56f6a2..610192b811029ba8455dbeafd98a1be682a7fb3e 100644
--- a/pd/src/m_glob.c
+++ b/pd/src/m_glob.c
@@ -79,13 +79,14 @@ static void glob_perf(t_pd *dummy, float f)
     sys_perf = (f != 0);
 }
 
-extern int sys_grid, sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init,
+extern int sys_snaptogrid, sys_gridsize, sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init,
     sys_autopatch_yoffset;
 extern t_symbol *sys_gui_preset;
 static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
 {
     sys_gui_preset = atom_getsymbolarg(0, argc--, argv++);
-    sys_grid = !!atom_getintarg(0, argc--, argv++);
+    sys_snaptogrid = !!atom_getintarg(0, argc--, argv++);
+    sys_gridsize = atom_getintarg(0, argc--, argv++);
     sys_zoom = !!atom_getintarg(0, argc--, argv++);
     sys_browser_doc = !!atom_getintarg(0, argc--, argv++);
     sys_browser_path = !!atom_getintarg(0, argc--, argv++);
@@ -96,10 +97,11 @@ static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
 /* just the gui-preset, the save-zoom toggle and various help browser options for now */
 static void glob_gui_properties(t_pd *dummy)
 {
-    gui_vmess("gui_gui_properties", "xsiiiiii",
+    gui_vmess("gui_gui_properties", "xsiiiiiii",
         dummy,
         sys_gui_preset->s_name,
-        sys_grid,
+        sys_snaptogrid,
+	sys_gridsize,
         sys_zoom,
         sys_browser_doc,
         sys_browser_path,
diff --git a/pd/src/s_file.c b/pd/src/s_file.c
index 0f0b18f258c23d9ad266a71eb86638fcb0ccd320..e78cabcc4ff0ae6ec6db62c4c6765ab6d7548de7 100644
--- a/pd/src/s_file.c
+++ b/pd/src/s_file.c
@@ -42,7 +42,7 @@
 #define snprintf sprintf_s
 #endif
 
-int sys_defeatrt, sys_autopatch_yoffset, sys_grid = 1, sys_zoom, sys_browser_doc = 1,
+int sys_defeatrt, sys_autopatch_yoffset, sys_snaptogrid = 1, sys_gridsize = 10, sys_zoom, sys_browser_doc = 1,
     sys_browser_path, sys_browser_init;
 t_symbol *sys_flags = &s_;
 void sys_doflags( void);
@@ -671,7 +671,9 @@ void sys_loadpreferences( void)
     if (sys_getpreference("defeatrt", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_defeatrt);
     if (sys_getpreference("showgrid", prefbuf, MAXPDSTRING))
-        sscanf(prefbuf, "%d", &sys_grid);
+        sscanf(prefbuf, "%d", &sys_snaptogrid);
+    if (sys_getpreference("gridsize", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_gridsize);
     if (sys_getpreference("savezoom", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_zoom);
     if (sys_getpreference("browser_doc", prefbuf, MAXPDSTRING))
@@ -819,8 +821,10 @@ void glob_savepreferences(t_pd *dummy)
     sys_putpreference("nloadlib", buf1);
     sprintf(buf1, "%d", sys_defeatrt);
     sys_putpreference("defeatrt", buf1);
-    sprintf(buf1, "%d", sys_grid);
+    sprintf(buf1, "%d", sys_snaptogrid);
     sys_putpreference("showgrid", buf1);
+    sprintf(buf1, "%d", sys_gridsize);
+    sys_putpreference("gridsize", buf1);
     sprintf(buf1, "%d", sys_zoom);
     sys_putpreference("savezoom", buf1);
     sprintf(buf1, "%d", sys_browser_doc);