diff --git a/__tests__/src/actions/canvas.test.js b/__tests__/src/actions/canvas.test.js
index b00c021700cd456cfebfb60acf9538216756cc90..481ff53f79f8a82a338a491f895d8155181cdcd7 100644
--- a/__tests__/src/actions/canvas.test.js
+++ b/__tests__/src/actions/canvas.test.js
@@ -33,4 +33,19 @@ describe('canvas actions', () => {
       expect(actions.setCanvas(id, 100)).toEqual(expectedAction);
     });
   });
+  describe('updateViewport', () => {
+    it('sets viewer state', () => {
+      const id = 'abc123';
+      const expectedAction = {
+        type: ActionTypes.UPDATE_VIEWPORT,
+        windowId: id,
+        payload: {
+          x: 1,
+          y: 0,
+          zoom: 0.5,
+        },
+      };
+      expect(actions.updateViewport(id, { x: 1, y: 0, zoom: 0.5 })).toEqual(expectedAction);
+    });
+  });
 });
diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js
index 72d2c8fa0bd87669e2f7d52943b628f9b07a287e..2e8c8347c2fbaf4af8d316d5e0c74eb066ee8b7e 100644
--- a/__tests__/src/components/OpenSeadragonViewer.test.js
+++ b/__tests__/src/components/OpenSeadragonViewer.test.js
@@ -1,15 +1,24 @@
 import React from 'react';
 import { shallow } from 'enzyme';
+import OpenSeadragon from 'openseadragon';
 import OpenSeadragonViewer from '../../../src/components/OpenSeadragonViewer';
 
+jest.mock('openseadragon');
+
 describe('OpenSeadragonViewer', () => {
   let wrapper;
+  let updateViewport;
   beforeEach(() => {
+    OpenSeadragon.mockClear();
+
+    updateViewport = jest.fn();
+
     wrapper = shallow(
       <OpenSeadragonViewer
         tileSources={[{ '@id': 'http://foo' }]}
         window={{ id: 'base' }}
         config={{}}
+        updateViewport={updateViewport}
       >
         <div className="foo" />
       </OpenSeadragonViewer>,
@@ -31,9 +40,6 @@ describe('OpenSeadragonViewer', () => {
   });
   describe('addTileSource', () => {
     it('calls addTiledImage asynchronously on the OSD viewer', async () => {
-      wrapper.instance().viewer = {
-        addTiledImage: jest.fn().mockResolvedValue('event'),
-      };
       wrapper.instance().addTileSource({}).then((event) => {
         expect(event).toBe('event');
       });
@@ -59,4 +65,93 @@ describe('OpenSeadragonViewer', () => {
       ).toHaveBeenCalled();
     });
   });
+
+  describe('componentDidMount', () => {
+    let panTo;
+    let zoomTo;
+    let addHandler;
+    beforeEach(() => {
+      panTo = jest.fn();
+      zoomTo = jest.fn();
+      addHandler = jest.fn();
+
+      wrapper = shallow(
+        <OpenSeadragonViewer
+          tileSources={[{ '@id': 'http://foo' }]}
+          window={{ id: 'base', viewer: { x: 1, y: 0, zoom: 0.5 } }}
+          config={{}}
+          updateViewport={updateViewport}
+        >
+          <div className="foo" />
+        </OpenSeadragonViewer>,
+      );
+
+      wrapper.instance().ref = { current: true };
+
+      OpenSeadragon.mockImplementation(() => ({
+        viewport: { panTo, zoomTo },
+        addHandler,
+        addTiledImage: jest.fn().mockResolvedValue('event'),
+      }));
+    });
+
+    it('calls the OSD viewport panTo and zoomTo with the component state', () => {
+      wrapper.instance().componentDidMount();
+
+      expect(addHandler).toHaveBeenCalledWith('viewport-change', expect.anything());
+
+      expect(panTo).toHaveBeenCalledWith(
+        { x: 1, y: 0, zoom: 0.5 }, false,
+      );
+      expect(zoomTo).toHaveBeenCalledWith(
+        0.5, { x: 1, y: 0, zoom: 0.5 }, false,
+      );
+    });
+  });
+
+  describe('componentDidUpdate', () => {
+    it('calls the OSD viewport panTo and zoomTo with the component state', () => {
+      const panTo = jest.fn();
+      const zoomTo = jest.fn();
+
+      wrapper.instance().viewer = {
+        viewport: {
+          centerSpringX: { target: { value: 10 } },
+          centerSpringY: { target: { value: 10 } },
+          zoomSpring: { target: { value: 1 } },
+          panTo,
+          zoomTo,
+        },
+      };
+
+      wrapper.setProps({ window: { id: 'base', viewer: { x: 0.5, y: 0.5, zoom: 0.1 } } });
+      wrapper.setProps({ window: { id: 'base', viewer: { x: 1, y: 0, zoom: 0.5 } } });
+
+      expect(panTo).toHaveBeenCalledWith(
+        { x: 1, y: 0, zoom: 0.5 }, false,
+      );
+      expect(zoomTo).toHaveBeenCalledWith(
+        0.5, { x: 1, y: 0, zoom: 0.5 }, false,
+      );
+    });
+  });
+
+  describe('onViewportChange', () => {
+    it('translates the OSD viewport data into an update to the component state', () => {
+      wrapper.instance().onViewportChange({
+        eventSource: {
+          viewport: {
+            centerSpringX: { target: { value: 1 } },
+            centerSpringY: { target: { value: 0 } },
+            zoomSpring: { target: { value: 0.5 } },
+          },
+        },
+      });
+
+      expect(updateViewport).toHaveBeenCalledWith(
+        'base',
+        { x: 1, y: 0, zoom: 0.5 },
+      );
+    });
+  });
 });
diff --git a/__tests__/src/reducers/windows.test.js b/__tests__/src/reducers/windows.test.js
index f7c09854bbb636c7aabe9650c6d75ed5609e9fc4..a8a75b6718dae4a218c7b2b06968a4e5bb541461 100644
--- a/__tests__/src/reducers/windows.test.js
+++ b/__tests__/src/reducers/windows.test.js
@@ -176,4 +176,27 @@ describe('windows reducer', () => {
       },
     });
   });
+
+  it('should handle UPDATE_VIEWPORT', () => {
+    expect(reducer({
+      abc123: {
+        id: 'abc123',
+      },
+      def456: {
+        id: 'def456',
+      },
+    }, {
+      type: ActionTypes.UPDATE_VIEWPORT,
+      windowId: 'abc123',
+      payload: { x: 0, y: 1, zoom: 0.5 },
+    })).toEqual({
+      abc123: {
+        id: 'abc123',
+        viewer: { x: 0, y: 1, zoom: 0.5 },
+      },
+      def456: {
+        id: 'def456',
+      },
+    });
+  });
 });
diff --git a/src/components/OpenSeadragonViewer.js b/src/components/OpenSeadragonViewer.js
index 316fcaae48afa9cb08ee49762e812d857d23f008..e28b87182dd4a77c31d216921901f3541356ffd8 100644
--- a/src/components/OpenSeadragonViewer.js
+++ b/src/components/OpenSeadragonViewer.js
@@ -16,13 +16,14 @@ class OpenSeadragonViewer extends Component {
 
     this.viewer = null;
     this.ref = React.createRef();
+    this.onViewportChange = this.onViewportChange.bind(this);
   }
 
   /**
    * React lifecycle event
    */
   componentDidMount() {
-    const { tileSources } = this.props;
+    const { tileSources, window } = this.props;
     if (!this.ref.current) {
       return;
     }
@@ -34,14 +35,22 @@ class OpenSeadragonViewer extends Component {
       showNavigationControl: false,
       preserveImageSizeOnResize: true,
     });
+    this.viewer.addHandler('viewport-change', this.onViewportChange);
+
+    if (window.viewer) {
+      this.viewer.viewport.panTo(window.viewer, false);
+      this.viewer.viewport.zoomTo(window.viewer.zoom, window.viewer, false);
+    }
+
     tileSources.forEach(tileSource => this.addTileSource(tileSource));
   }
 
   /**
    * When the tileSources change, make sure to close the OSD viewer.
+   * When the viewport state changes, pan or zoom the OSD viewer as appropriate
    */
   componentDidUpdate(prevProps) {
-    const { tileSources } = this.props;
+    const { tileSources, window } = this.props;
     if (!this.tileSourcesMatch(prevProps.tileSources)) {
       this.viewer.close();
       Promise.all(
@@ -53,6 +62,17 @@ class OpenSeadragonViewer extends Component {
           this.fitBounds(0, 0, tileSources[0].width, tileSources[0].height);
         }
       });
+    } else if (window.viewer) {
+      const { viewport } = this.viewer;
+
+      if (window.viewer.x !== viewport.centerSpringX.target.value
+        || window.viewer.y !== viewport.centerSpringY.target.value) {
+        this.viewer.viewport.panTo(window.viewer, false);
+      }
+
+      if (window.viewer.zoom !== viewport.zoomSpring.target.value) {
+        this.viewer.viewport.zoomTo(window.viewer.zoom, window.viewer, false);
+      }
     }
   }
 
@@ -62,6 +82,21 @@ class OpenSeadragonViewer extends Component {
     this.viewer.removeAllHandlers();
   }
 
+  /**
+   * Forward OSD state to redux
+   */
+  onViewportChange(event) {
+    const { updateViewport, window } = this.props;
+
+    const { viewport } = event.eventSource;
+
+    updateViewport(window.id, {
+      x: viewport.centerSpringX.target.value,
+      y: viewport.centerSpringY.target.value,
+      zoom: viewport.zoomSpring.target.value,
+    });
+  }
+
   /**
    */
   addTileSource(tileSource) {
@@ -133,6 +168,7 @@ OpenSeadragonViewer.propTypes = {
   children: PropTypes.element,
   tileSources: PropTypes.arrayOf(PropTypes.object),
   window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+  updateViewport: PropTypes.func.isRequired,
 };
 
 export default OpenSeadragonViewer;
diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js
index eecd30d771a508f52f08d4b91e7ac95caed5621a..3584aa3194c43e78148882892e1ca99f9a069255 100644
--- a/src/containers/OpenSeadragonViewer.js
+++ b/src/containers/OpenSeadragonViewer.js
@@ -1,4 +1,23 @@
+import { compose } from 'redux';
+import { connect } from 'react-redux';
 import miradorWithPlugins from '../lib/miradorWithPlugins';
 import OpenSeadragonViewer from '../components/OpenSeadragonViewer';
+import * as actions from '../state/actions';
 
-export default miradorWithPlugins(OpenSeadragonViewer);
+/**
+ * mapDispatchToProps - used to hook up connect to action creators
+ * @memberof ManifestListItem
+ * @private
+ */
+const mapDispatchToProps = {
+  updateViewport: actions.updateViewport,
+};
+
+const enhance = compose(
+  connect(null, mapDispatchToProps),
+  miradorWithPlugins,
+  // further HOC go here
+);
+
+
+export default enhance(OpenSeadragonViewer);
diff --git a/src/containers/Workspace.js b/src/containers/Workspace.js
index 0bac3285eb2f9364e1f08423dd0dcad13d6013a9..a86a81739d1cb8099d0ce6e70ced1b4df2e9e302 100644
--- a/src/containers/Workspace.js
+++ b/src/containers/Workspace.js
@@ -1,6 +1,5 @@
 import { compose } from 'redux';
 import { connect } from 'react-redux';
-import * as actions from '../state/actions';
 import Workspace from '../components/Workspace';
 
 /**
diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js
index d21f58b39f2ea2ba9f63915cbde1c11e636abeb3..50869227446a4af8ac34f903b91d48e2e710df3b 100644
--- a/src/state/actions/action-types.js
+++ b/src/state/actions/action-types.js
@@ -22,6 +22,7 @@ const ActionTypes = {
   RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
   REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
   UPDATE_WORKSPACE_MOSAIC_LAYOUT: 'UPDATE_WORKSPACE_MOSAIC_LAYOUT',
+  UPDATE_VIEWPORT: 'UPDATE_VIEWPORT',
 };
 
 export default ActionTypes;
diff --git a/src/state/actions/canvas.js b/src/state/actions/canvas.js
index 9e094aa4d3573f5b9f39c87d9f9d47cbd904915c..c1a5f1ad9b26343317e840e7720eb85a6ae3371c 100644
--- a/src/state/actions/canvas.js
+++ b/src/state/actions/canvas.js
@@ -30,3 +30,14 @@ export function previousCanvas(windowId) {
 export function setCanvas(windowId, canvasIndex) {
   return { type: ActionTypes.SET_CANVAS, windowId, canvasIndex };
 }
+
+/**
+ * updateViewport - action creator
+ *
+ * @param  {String} windowId
+ * @param  {Number} canvasIndex
+ * @memberof ActionCreators
+ */
+export function updateViewport(windowId, payload) {
+  return { type: ActionTypes.UPDATE_VIEWPORT, windowId, payload };
+}
diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js
index 26c18f70220ba79a9a7d974e17e52434e7176679..108f77d2209d00d2586d673316b6ee3c64db300d 100644
--- a/src/state/reducers/windows.js
+++ b/src/state/reducers/windows.js
@@ -49,6 +49,14 @@ const windowsReducer = (state = {}, action) => {
       return setCanvasIndex(state, action.windowId, currentIndex => currentIndex - 1);
     case ActionTypes.SET_CANVAS:
       return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex);
+    case ActionTypes.UPDATE_VIEWPORT:
+      return {
+        ...state,
+        [action.windowId]: {
+          ...state[action.windowId],
+          viewer: action.payload,
+        },
+      };
     default:
       return state;
   }