diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js
index 6368e2e050d1ea528cf051d5649d4c9359955943..8113ca8e9bfbda73b9526d85e757ff14710bd62f 100644
--- a/__tests__/src/components/OpenSeadragonViewer.test.js
+++ b/__tests__/src/components/OpenSeadragonViewer.test.js
@@ -2,15 +2,20 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import OpenSeadragon from 'openseadragon';
 import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer';
+import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay';
+import Annotation from '../../../src/lib/Annotation';
 import ZoomControls from '../../../src/containers/ZoomControls';
 
 jest.mock('openseadragon');
+jest.mock('../../../src/lib/OpenSeadragonCanvasOverlay');
+
 
 describe('OpenSeadragonViewer', () => {
   let wrapper;
   let updateViewport;
   beforeEach(() => {
     OpenSeadragon.mockClear();
+    OpenSeadragonCanvasOverlay.mockClear();
 
     updateViewport = jest.fn();
 
@@ -126,6 +131,16 @@ describe('OpenSeadragonViewer', () => {
         0.5, { x: 1, y: 0, zoom: 0.5 }, false,
       );
     });
+
+    it('sets up a OpenSeadragonCanvasOverlay', () => {
+      wrapper.instance().componentDidMount();
+      expect(OpenSeadragonCanvasOverlay).toHaveBeenCalledTimes(1);
+    });
+
+    it('sets up a listener on update-viewport', () => {
+      wrapper.instance().componentDidMount();
+      expect(addHandler).toHaveBeenCalledWith('update-viewport', expect.anything());
+    });
   });
 
   describe('componentDidUpdate', () => {
@@ -153,6 +168,49 @@ describe('OpenSeadragonViewer', () => {
         0.5, { x: 1, y: 0, zoom: 0.5 }, false,
       );
     });
+
+    it('sets up canvasUpdate to add annotations to the canvas', () => {
+      const clear = jest.fn();
+      const resize = jest.fn();
+      const canvasUpdate = jest.fn();
+      wrapper.instance().osdCanvasOverlay = {
+        clear,
+        resize,
+        canvasUpdate,
+      };
+
+      wrapper.setProps(
+        {
+          annotations: [
+            new Annotation(
+              { '@id': 'foo', resources: [{ foo: 'bar' }] },
+            ),
+          ],
+        },
+      );
+      wrapper.setProps(
+        {
+          annotations: [
+            new Annotation(
+              { '@id': 'foo', resources: [{ foo: 'bar' }] },
+            ),
+          ],
+        },
+      );
+      wrapper.setProps(
+        {
+          annotations: [
+            new Annotation(
+              { '@id': 'bar', resources: [{ foo: 'bar' }] },
+            ),
+          ],
+        },
+      );
+      wrapper.instance().updateCanvas();
+      expect(clear).toHaveBeenCalledTimes(1);
+      expect(resize).toHaveBeenCalledTimes(1);
+      expect(canvasUpdate).toHaveBeenCalledTimes(1);
+    });
   });
 
   describe('onViewportChange', () => {
@@ -173,4 +231,35 @@ describe('OpenSeadragonViewer', () => {
       );
     });
   });
+
+  describe('onUpdateViewport', () => {
+    it('fires updateCanvas', () => {
+      const updateCanvas = jest.fn();
+      wrapper.instance().updateCanvas = updateCanvas;
+      wrapper.instance().onUpdateViewport();
+      expect(updateCanvas).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('annotationsToContext', () => {
+    it('converts the annotations to canvas', () => {
+      const strokeRect = jest.fn();
+      wrapper.instance().osdCanvasOverlay = {
+        context2d: {
+          strokeRect,
+        },
+      };
+
+      const annotations = [
+        new Annotation(
+          { '@id': 'foo', resources: [{ on: 'www.example.com/#xywh=10,10,100,200' }] },
+        ),
+      ];
+      wrapper.instance().annotationsToContext(annotations);
+      const context = wrapper.instance().osdCanvasOverlay.context2d;
+      expect(context.strokeStyle).toEqual('yellow');
+      expect(context.lineWidth).toEqual(10);
+      expect(strokeRect).toHaveBeenCalledWith(10, 10, 100, 200);
+    });
+  });
 });
diff --git a/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..8663b652be436724cd7a52bb9191aa530e688a4e
--- /dev/null
+++ b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js
@@ -0,0 +1,122 @@
+import OpenSeadragon from 'openseadragon';
+import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay';
+
+jest.mock('openseadragon');
+
+describe('OpenSeadragonCanvasOverlay', () => {
+  let canvasOverlay;
+  beforeEach(() => {
+    document.body.innerHTML = '<div id="canvas"></div>';
+    OpenSeadragon.mockClear();
+    OpenSeadragon.mockImplementation(() => ({
+      canvas: document.getElementById('canvas'),
+      container: {
+        clientHeight: 100,
+        clientWidth: 200,
+      },
+      viewport: {
+        getBounds: jest.fn(() => ({
+          x: 40, y: 80, width: 200, height: 300,
+        })),
+        getZoom: jest.fn(() => (0.75)),
+      },
+      world: {
+        getItemAt: jest.fn(() => ({
+          source: {
+            dimensions: {
+              x: 1000,
+              y: 2000,
+            },
+          },
+          viewportToImageZoom: jest.fn(() => (0.075)),
+        })),
+      },
+    }));
+    canvasOverlay = new OpenSeadragonCanvasOverlay(new OpenSeadragon());
+  });
+  describe('constructor', () => {
+    it('sets up initial values and canvas', () => {
+      expect(canvasOverlay.containerHeight).toEqual(0);
+      expect(canvasOverlay.containerWidth).toEqual(0);
+      expect(canvasOverlay.canvasDiv.outerHTML).toEqual(
+        '<div style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"><canvas></canvas></div>',
+      );
+    });
+  });
+  describe('context2d', () => {
+    it('calls getContext on canvas', () => {
+      const contextMock = jest.fn();
+      canvasOverlay.canvas = {
+        getContext: contextMock,
+      };
+      canvasOverlay.context2d; // eslint-disable-line no-unused-expressions
+      expect(contextMock).toHaveBeenCalledTimes(1);
+    });
+  });
+  describe('clear', () => {
+    it('calls getContext and clearRect on canvas', () => {
+      const clearRect = jest.fn();
+      const contextMock = jest.fn(() => ({
+        clearRect,
+      }));
+      canvasOverlay.canvas = {
+        getContext: contextMock,
+      };
+      canvasOverlay.clear();
+      expect(contextMock).toHaveBeenCalledTimes(1);
+      expect(clearRect).toHaveBeenCalledTimes(1);
+    });
+  });
+  describe('resize', () => {
+    it('sets various values based off of image and container sizes', () => {
+      canvasOverlay.resize();
+      expect(canvasOverlay.containerHeight).toEqual(100);
+      expect(canvasOverlay.containerWidth).toEqual(200);
+      expect(canvasOverlay.imgAspectRatio).toEqual(0.5);
+    });
+    it('when image is undefined returns early', () => {
+      OpenSeadragon.mockClear();
+      OpenSeadragon.mockImplementation(() => ({
+        canvas: document.getElementById('canvas'),
+        container: {
+          clientHeight: 100,
+          clientWidth: 200,
+        },
+        viewport: {
+          getBounds: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))),
+        },
+        world: {
+          getItemAt: jest.fn(),
+        },
+      }));
+      canvasOverlay = new OpenSeadragonCanvasOverlay(new OpenSeadragon());
+      canvasOverlay.resize();
+      expect(canvasOverlay.imgHeight).toEqual(undefined);
+      expect(canvasOverlay.imgWidth).toEqual(undefined);
+    });
+  });
+  describe('canvasUpdate', () => {
+    it('sets appropriate sizes and calls update argument', () => {
+      const scale = jest.fn();
+      const setAttribute = jest.fn();
+      const setTransform = jest.fn();
+      const translate = jest.fn();
+      const contextMock = jest.fn(() => ({
+        scale,
+        setTransform,
+        translate,
+      }));
+      canvasOverlay.canvas = {
+        getContext: contextMock,
+        setAttribute,
+      };
+      const update = jest.fn();
+      canvasOverlay.resize();
+      canvasOverlay.canvasUpdate(update);
+      expect(update).toHaveBeenCalledTimes(1);
+      expect(scale).toHaveBeenCalledWith(0.075, 0.075);
+      expect(translate).toHaveBeenCalledWith(-39.96, -26.65333333333333);
+      expect(setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 0, 0);
+    });
+  });
+});
diff --git a/src/components/OpenSeadragonViewer.js b/src/components/OpenSeadragonViewer.js
index 6428c77e0f492988c24d5741f01eefdd8c6ae9a1..1a265470570e3504242f542c8256d30c32c7c710 100644
--- a/src/components/OpenSeadragonViewer.js
+++ b/src/components/OpenSeadragonViewer.js
@@ -4,6 +4,7 @@ import OpenSeadragon from 'openseadragon';
 import debounce from 'lodash/debounce';
 import ns from '../config/css-ns';
 import ZoomControls from '../containers/ZoomControls';
+import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
 
 /**
  * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
@@ -17,7 +18,11 @@ export class OpenSeadragonViewer extends Component {
     super(props);
 
     this.viewer = null;
+    this.osdCanvasOverlay = null;
+    // An initial value for the updateCanvas method
+    this.updateCanvas = () => {};
     this.ref = React.createRef();
+    this.onUpdateViewport = this.onUpdateViewport.bind(this);
     this.onViewportChange = this.onViewportChange.bind(this);
   }
 
@@ -37,6 +42,9 @@ export class OpenSeadragonViewer extends Component {
       showNavigationControl: false,
       preserveImageSizeOnResize: true,
     });
+
+    this.osdCanvasOverlay = new OpenSeadragonCanvasOverlay(this.viewer);
+    this.viewer.addHandler('update-viewport', this.onUpdateViewport);
     this.viewer.addHandler('viewport-change', debounce(this.onViewportChange, 300));
 
     if (viewer) {
@@ -49,10 +57,21 @@ export class OpenSeadragonViewer extends Component {
 
   /**
    * When the tileSources change, make sure to close the OSD viewer.
+   * When the annotations change, reset the updateCanvas method to make sure
+   * they are added.
    * When the viewport state changes, pan or zoom the OSD viewer as appropriate
    */
   componentDidUpdate(prevProps) {
-    const { tileSources, viewer } = this.props;
+    const { tileSources, viewer, annotations } = this.props;
+    if (!this.annotationsMatch(prevProps.annotations)) {
+      this.updateCanvas = () => {
+        this.osdCanvasOverlay.clear();
+        this.osdCanvasOverlay.resize();
+        this.osdCanvasOverlay.canvasUpdate(() => {
+          this.annotationsToContext(annotations);
+        });
+      };
+    }
     if (!this.tileSourcesMatch(prevProps.tileSources)) {
       this.viewer.close();
       Promise.all(
@@ -82,6 +101,13 @@ export class OpenSeadragonViewer extends Component {
     this.viewer.removeAllHandlers();
   }
 
+  /**
+   * onUpdateViewport - fires during OpenSeadragon render method.
+   */
+  onUpdateViewport(event) {
+    this.updateCanvas();
+  }
+
   /**
    * Forward OSD state to redux
    */
@@ -97,6 +123,20 @@ export class OpenSeadragonViewer extends Component {
     });
   }
 
+  /**
+   * annotationsToContext - converts anontations to a canvas context
+   */
+  annotationsToContext(annotations) {
+    const context = this.osdCanvasOverlay.context2d;
+    annotations.forEach((annotation) => {
+      annotation.resources.forEach((resource) => {
+        context.strokeStyle = 'yellow';
+        context.lineWidth = 10;
+        context.strokeRect(...resource.fragmentSelector);
+      });
+    });
+  }
+
   /**
    * boundsFromTileSources - calculates the overall width/height
    * based on 0 -> n tileSources
@@ -196,6 +236,25 @@ export class OpenSeadragonViewer extends Component {
     });
   }
 
+  /**
+   * annotationsMatch - compares previous annotations to current to determine
+   * whether to add a new updateCanvas method to draw annotations
+   * @param  {Array} prevAnnotations
+   * @return {Boolean}
+   */
+  annotationsMatch(prevAnnotations) {
+    const { annotations } = this.props;
+    return annotations.some((annotation, index) => {
+      if (!prevAnnotations[index]) {
+        return false;
+      }
+      if (annotation.id === prevAnnotations[index].id) {
+        return true;
+      }
+      return false;
+    });
+  }
+
   /**
    * Renders things
    */
@@ -221,6 +280,7 @@ export class OpenSeadragonViewer extends Component {
 }
 
 OpenSeadragonViewer.defaultProps = {
+  annotations: [],
   children: null,
   tileSources: [],
   viewer: null,
@@ -228,6 +288,7 @@ OpenSeadragonViewer.defaultProps = {
 };
 
 OpenSeadragonViewer.propTypes = {
+  annotations: PropTypes.arrayOf(PropTypes.object),
   children: PropTypes.element,
   tileSources: PropTypes.arrayOf(PropTypes.object),
   viewer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js
index 9f103823ffff31dfbae01d24b8a8d01267eb0d4b..78b5354560789a897ae934acd703cdb380deb9db 100644
--- a/src/components/WindowViewer.js
+++ b/src/components/WindowViewer.js
@@ -126,6 +126,7 @@ export class WindowViewer extends Component {
       <>
         <OSDViewer
           tileSources={this.tileInfoFetchedFromStore()}
+          currentCanvases={this.currentCanvases()}
           windowId={window.id}
         >
           <ViewerNavigation window={window} canvases={this.canvases} />
diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js
index eb0e1422d496870e33c228ab101a5bd0ffdc238e..7099f30dc6460276366607398747635cf8499a66 100644
--- a/src/containers/OpenSeadragonViewer.js
+++ b/src/containers/OpenSeadragonViewer.js
@@ -7,6 +7,7 @@ import * as actions from '../state/actions';
 import {
   getCanvasLabel,
   getSelectedCanvas,
+  getSelectedCanvasAnnotations,
 } from '../state/selectors';
 
 /**
@@ -14,12 +15,18 @@ import {
  * @memberof Window
  * @private
  */
-const mapStateToProps = ({ viewers, windows, manifests }, { windowId }) => ({
+const mapStateToProps = ({
+  viewers, windows, manifests, annotations,
+}, { windowId, currentCanvases }) => ({
   viewer: viewers[windowId],
   label: getCanvasLabel(
     getSelectedCanvas({ windows, manifests }, windowId),
     windows[windowId].canvasIndex,
   ),
+  annotations: getSelectedCanvasAnnotations(
+    { annotations },
+    currentCanvases.map(canvas => canvas.id),
+  ),
 });
 
 /**
diff --git a/src/lib/OpenSeadragonCanvasOverlay.js b/src/lib/OpenSeadragonCanvasOverlay.js
new file mode 100644
index 0000000000000000000000000000000000000000..e306f4579b84557503324fb6d9c33676ca56c5b1
--- /dev/null
+++ b/src/lib/OpenSeadragonCanvasOverlay.js
@@ -0,0 +1,100 @@
+import OpenSeadragon from 'openseadragon';
+
+/**
+ * OpenSeadragonCanvasOverlay - adapted from https://github.com/altert/OpenSeadragonCanvasOverlay
+ * used rather than an "onRedraw" function we tap into our own method. Existing
+ * repository is not published as an npm package.
+ * Code ported from https://github.com/altert/OpenSeadragonCanvasOverlay
+ * carries a BSD 3-Clause license originally authored by @altert from
+ * https://github.com/altert/OpenseadragonFabricjsOverlay
+ */
+export default class OpenSeadragonCanvasOverlay {
+  /**
+   * constructor - sets up the Canvas overlay container
+   */
+  constructor(viewer) {
+    this.viewer = viewer;
+
+    this.containerWidth = 0;
+    this.containerHeight = 0;
+
+    this.canvasDiv = document.createElement('div');
+    this.canvasDiv.style.position = 'absolute';
+    this.canvasDiv.style.left = 0;
+    this.canvasDiv.style.top = 0;
+    this.canvasDiv.style.width = '100%';
+    this.canvasDiv.style.height = '100%';
+    this.viewer.canvas.appendChild(this.canvasDiv);
+
+    this.canvas = document.createElement('canvas');
+    this.canvasDiv.appendChild(this.canvas);
+    this.imgAspectRatio = 1;
+  }
+
+  /** */
+  get context2d() {
+    return this.canvas.getContext('2d');
+  }
+
+  /** */
+  clear() {
+    this.canvas.getContext('2d').clearRect(0, 0, this.containerWidth, this.containerHeight);
+  }
+
+  /**
+   * resize - resizes the added Canvas overlay.
+   */
+  resize() {
+    if (this.containerWidth !== this.viewer.container.clientWidth) {
+      this.containerWidth = this.viewer.container.clientWidth;
+      this.canvasDiv.setAttribute('width', this.containerWidth);
+      this.canvas.setAttribute('width', this.containerWidth);
+    }
+
+    if (this.containerHeight !== this.viewer.container.clientHeight) {
+      this.containerHeight = this.viewer.container.clientHeight;
+      this.canvasDiv.setAttribute('height', this.containerHeight);
+      this.canvas.setAttribute('height', this.containerHeight);
+    }
+
+    this.viewportOrigin = new OpenSeadragon.Point(0, 0);
+    const boundsRect = this.viewer.viewport.getBounds(true);
+    this.viewportOrigin.x = boundsRect.x;
+    this.viewportOrigin.y = boundsRect.y * this.imgAspectRatio;
+
+    this.viewportWidth = boundsRect.width;
+    this.viewportHeight = boundsRect.height * this.imgAspectRatio;
+    const image1 = this.viewer.world.getItemAt(0);
+    if (!image1) return;
+    this.imgWidth = image1.source.dimensions.x;
+    this.imgHeight = image1.source.dimensions.y;
+    this.imgAspectRatio = this.imgWidth / this.imgHeight;
+  }
+
+  /**
+   * canvasUpdate - sets up the dimensions for the canvas update to mimick image
+   * 0 dimensions. Then call provided update function.
+   * @param {Function} update
+   */
+  canvasUpdate(update) {
+    const viewportZoom = this.viewer.viewport.getZoom(true);
+    const image1 = this.viewer.world.getItemAt(0);
+    if (!image1) return;
+    const zoom = image1.viewportToImageZoom(viewportZoom);
+
+    const x = (
+      (this.viewportOrigin.x / this.imgWidth - this.viewportOrigin.x) / this.viewportWidth
+    ) * this.containerWidth;
+    const y = (
+      (this.viewportOrigin.y / this.imgHeight - this.viewportOrigin.y) / this.viewportHeight
+    ) * this.containerHeight;
+
+    if (this.clearBeforeRedraw) this.clear();
+    this.canvas.getContext('2d').translate(x, y);
+    this.canvas.getContext('2d').scale(zoom, zoom);
+
+    update();
+
+    this.canvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
+  }
+}