diff --git a/README.md b/README.md
index f784300a..ee14d98a 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,18 @@ function constrain(transform, extent, translateExtent) {
 
 The constraint function must return a [*transform*](#zoom-transforms) given the current *transform*, [viewport extent](#zoom_extent) and [translate extent](#zoom_translateExtent). The default implementation attempts to ensure that the viewport extent does not go outside the translate extent.
 
+<a href="#zoom_center" name="zoom_center">#</a> <i>zoom</i>.<b>center</b>([<i>center</i>]) · [Source](https://github.com/d3/d3-zoom/blob/master/src/zoom.js), [Examples](https://observablehq.com/d/48ff4a64c477cc79)
+
+If *center* is a function, sets the center to the specified function, and returns the zoom behavior. If *center* is an array, sets the center to a constant function that returns the specified array. If *center* is not specified, returns the current center, which defaults to the pointer’s position relative to the current DOM element:
+
+```js
+function center(event) {
+  return d3.pointer(event, this);
+}
+```
+
+The center is passed the current event (`event`) and datum `d`, with the `this` context as the current DOM element. It must return the reference position as [*x*, *y*].
+
 <a href="#zoom_filter" name="zoom_filter">#</a> <i>zoom</i>.<b>filter</b>([<i>filter</i>]) · [Source](https://github.com/d3/d3-zoom/blob/master/src/zoom.js)
 
 If *filter* is specified, sets the filter to the specified function and returns the zoom behavior. If *filter* is not specified, returns the current filter, which defaults to:
diff --git a/src/zoom.js b/src/zoom.js
index fa959025..b3c5d194 100644
--- a/src/zoom.js
+++ b/src/zoom.js
@@ -13,6 +13,10 @@ function defaultFilter(event) {
   return !event.ctrlKey && !event.button;
 }
 
+function defaultCenter(event) {
+  return pointer(event, this);
+}
+
 function defaultExtent() {
   var e = this;
   if (e instanceof SVGElement) {
@@ -51,6 +55,7 @@ function defaultConstrain(transform, extent, translateExtent) {
 
 export default function() {
   var filter = defaultFilter,
+      center = defaultCenter,
       extent = defaultExtent,
       constrain = defaultConstrain,
       wheelDelta = defaultWheelDelta,
@@ -243,6 +248,7 @@ export default function() {
     if (g.wheel) {
       if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {
         g.mouse[1] = t.invert(g.mouse[0] = p);
+        g.mouse[2] = center.apply(this, arguments);
       }
       clearTimeout(g.wheel);
     }
@@ -252,14 +258,14 @@ export default function() {
 
     // Otherwise, capture the mouse point and location at the start.
     else {
-      g.mouse = [p, t.invert(p)];
+      g.mouse = [p, t.invert(p), center.apply(this, arguments)];
       interrupt(this);
       g.start();
     }
 
     noevent(event);
     g.wheel = setTimeout(wheelidled, wheelDelay);
-    g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));
+    g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[2], t.invert(g.mouse[2])), g.extent, translateExtent));
 
     function wheelidled() {
       g.wheel = null;
@@ -404,6 +410,10 @@ export default function() {
     return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), zoom) : touchable;
   };
 
+  zoom.center = function(_) {
+    return arguments.length ? (center = typeof _ === "function" ? _ : constant([+_[0], +_[1]]), zoom) : center;
+  };
+
   zoom.extent = function(_) {
     return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
   };