diff --git a/__tests__/src/components/CanvasThumbnail.test.js b/__tests__/src/components/CanvasThumbnail.test.js
index f3fa0904adba3805658180cf95f5ec4df2130b45..9590dd3fa66ed87cdb55da6850c68cbada1f5a97 100644
--- a/__tests__/src/components/CanvasThumbnail.test.js
+++ b/__tests__/src/components/CanvasThumbnail.test.js
@@ -3,15 +3,22 @@ import { shallow } from 'enzyme';
 import IntersectionObserver from '@researchgate/react-intersection-observer';
 import { CanvasThumbnail } from '../../../src/components/CanvasThumbnail';
 
+/**
+ * Helper function to create a shallow wrapper around CanvasThumbnail
+ */
+function createWrapper(props) {
+  return shallow(
+    <CanvasThumbnail
+      imageUrl="https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/full/193,/0/default.jpg"
+      {...props}
+    />,
+  );
+}
+
 describe('CanvasThumbnail', () => {
   let wrapper;
   beforeEach(() => {
-    wrapper = shallow(
-      <CanvasThumbnail
-        imageUrl="https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/full/193,/0/default.jpg"
-        onClick={() => {}}
-      />,
-    );
+    wrapper = createWrapper();
   });
 
   it('renders properly', () => {
@@ -32,4 +39,30 @@ describe('CanvasThumbnail', () => {
     wrapper.instance().handleIntersection({ isIntersecting: true });
     expect(wrapper.find('img').props().src).toMatch(/stacks/);
   });
+
+  it('can be constrained by maxHeight', () => {
+    wrapper = createWrapper({ maxHeight: 500 });
+
+    expect(wrapper.find('img').props().style).toMatchObject({ height: 500, width: 'auto' });
+  });
+
+  it('can be constrained by maxWidth', () => {
+    wrapper = createWrapper({ maxWidth: 500 });
+
+    expect(wrapper.find('img').props().style).toMatchObject({ height: 'auto', width: 500 });
+  });
+
+  it('can be constrained by maxWidth and maxHeight', () => {
+    wrapper = createWrapper({ maxHeight: 400, maxWidth: 500 });
+
+    expect(wrapper.find('img').props().style).toMatchObject({ height: 400, width: 500 });
+  });
+
+  it('can be constrained by maxWidth and maxHeight and a desired aspect ratio', () => {
+    wrapper = createWrapper({ maxHeight: 400, maxWidth: 500, aspectRatio: 2 });
+    expect(wrapper.find('img').props().style).toMatchObject({ height: 250, width: 500 });
+
+    wrapper = createWrapper({ maxHeight: 400, maxWidth: 500, aspectRatio: 1 });
+    expect(wrapper.find('img').props().style).toMatchObject({ height: 400, width: 400 });
+  });
 });
diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js
index 133465a26840f422cbc32593703b37b4e72e1949..19ca09079c501dde70a1f51d1fbd0d6f90adb66c 100644
--- a/__tests__/src/components/ThumbnailNavigation.test.js
+++ b/__tests__/src/components/ThumbnailNavigation.test.js
@@ -52,7 +52,7 @@ describe('ThumbnailNavigation', () => {
     expect(wrapper.find('.mirador-thumbnail-nav-canvas-1.mirador-current-canvas'));
   });
   it('when clicked, updates the current canvas', () => {
-    renderedGrid.find('.mirador-thumbnail-nav-canvas-0 CanvasThumbnail').simulate('click');
+    renderedGrid.find('.mirador-thumbnail-nav-canvas-0 WithStyles(GridListTile)').simulate('click');
     expect(setCanvas).toHaveBeenCalledWith('foobar', 0);
   });
   it('sets up calculated width based off of height of area and dimensions of canvas', () => {
diff --git a/__tests__/src/components/WindowSideBarCanvasPanel.test.js b/__tests__/src/components/WindowSideBarCanvasPanel.test.js
index 14bbf84443cf53ab80b467971eee0b4537565fae..a5937e1c9e6df98d804722df62c372b1c7780d50 100644
--- a/__tests__/src/components/WindowSideBarCanvasPanel.test.js
+++ b/__tests__/src/components/WindowSideBarCanvasPanel.test.js
@@ -4,7 +4,6 @@ import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import Typography from '@material-ui/core/Typography';
 import manifesto from 'manifesto.js';
-import { CanvasThumbnail } from '../../../src/components/CanvasThumbnail';
 import { WindowSideBarCanvasPanel } from '../../../src/components/WindowSideBarCanvasPanel';
 import manifestJson from '../../fixtures/version-2/019.json';
 import { getIdAndLabelOfCanvases } from '../../../src/state/selectors';
@@ -54,13 +53,8 @@ describe('WindowSideBarCanvasPanel', () => {
       .text()).toBe(idsAndLabels[1].label);
   });
 
-  it('should call the onClick handler of a navigation item\'s label', () => {
-    wrapper.find(Typography).at(1).simulate('click');
-    expect(setCanvas).toHaveBeenCalledTimes(1);
-  });
-
-  it('should call the onClick handler of a navigation item\'s thumbnail', () => {
-    wrapper.find(CanvasThumbnail).at(0).simulate('click');
+  it('should call the onClick handler of a list item', () => {
+    wrapper.find(ListItem).at(1).simulate('click');
     expect(setCanvas).toHaveBeenCalledTimes(1);
   });
 });
diff --git a/__tests__/src/lib/ManifestoCanvas.test.js b/__tests__/src/lib/ManifestoCanvas.test.js
index 7a3c8db79d83cac6371700a15b1290bf9ab686a7..ef43ad1c765850821ea4f25bea4eff436a64b91a 100644
--- a/__tests__/src/lib/ManifestoCanvas.test.js
+++ b/__tests__/src/lib/ManifestoCanvas.test.js
@@ -43,14 +43,28 @@ describe('ManifestoCanvas', () => {
     });
   });
   describe('thumbnail', () => {
-    it('calculates a thumbnail image API request based off of height', () => {
+    it('calculates a thumbnail image API request based off of width, height and aspect ratio', () => {
+      expect(instance.thumbnail(100, 100)).toEqual(
+        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/,100/0/default.jpg',
+      );
+
+      expect(instance.thumbnail(100, 1000)).toEqual(
+        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/100,/0/default.jpg',
+      );
+    });
+    it('calculates a thumbnail image API request based off of width', () => {
       expect(instance.thumbnail(100)).toEqual(
-        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/66,/0/default.jpg',
+        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/100,/0/default.jpg',
+      );
+    });
+    it('calculates a thumbnail image API request based off of height', () => {
+      expect(instance.thumbnail(null, 100)).toEqual(
+        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/,100/0/default.jpg',
       );
     });
     it('defaults to using 150 as a height', () => {
       expect(instance.thumbnail()).toEqual(
-        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/100,/0/default.jpg',
+        'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/,150/0/default.jpg',
       );
     });
 
diff --git a/src/components/CanvasThumbnail.js b/src/components/CanvasThumbnail.js
index 87a03bbc15557e3b202903a2131371f2e3496a94..9e1fd254928ac7b106745dcdff9235d20b5fefda 100644
--- a/src/components/CanvasThumbnail.js
+++ b/src/components/CanvasThumbnail.js
@@ -45,16 +45,63 @@ export class CanvasThumbnail extends Component {
     return CanvasThumbnail.defaultImgPlaceholder;
   }
 
+  /** */
+  imageConstraints() {
+    const {
+      maxHeight, maxWidth, aspectRatio,
+    } = this.props;
+
+    if (maxHeight && maxWidth && aspectRatio) return 'sizeByConfinedWh';
+    if (maxHeight && maxWidth) return 'sizeByDistortedWh';
+    if (maxHeight && !maxWidth) return 'sizeByH';
+    if (!maxHeight && maxWidth) return 'sizeByW';
+
+    return undefined;
+  }
+
   /**
    *
   */
   imageStyles() {
-    const { height, style } = this.props;
-    const { image } = this.state;
+    const {
+      maxHeight, maxWidth, aspectRatio, style,
+    } = this.props;
+
+    let height;
+    let width;
+
+    switch (this.imageConstraints()) {
+      case 'sizeByConfinedWh':
+        // size to width
+        if ((maxWidth / maxHeight) < aspectRatio) {
+          height = maxWidth / aspectRatio;
+          width = maxWidth;
+        } else {
+          height = maxHeight;
+          width = maxHeight * aspectRatio;
+        }
+
+        break;
+      case 'sizeByDistortedWh':
+        height = maxHeight;
+        width = maxWidth;
+        break;
+      case 'sizeByH':
+        height = maxHeight;
+        width = 'auto';
+        break;
+      case 'sizeByW':
+        height = 'auto';
+        width = maxWidth;
+        break;
+      default:
+        height = 'auto';
+        width = 'auto';
+    }
 
     return {
       height,
-      width: (image && image.src) ? '100%' : '110px',
+      width,
       ...style,
     };
   }
@@ -62,14 +109,11 @@ export class CanvasThumbnail extends Component {
   /**
    */
   render() {
-    const { onClick } = this.props;
     return (
       <>
         <IntersectionObserver onChange={this.handleIntersection}>
           <img
             alt=""
-            onClick={onClick}
-            onKeyPress={onClick}
             role="presentation"
             src={this.imageSrc()}
             style={this.imageStyles()}
@@ -86,14 +130,17 @@ CanvasThumbnail.defaultImgPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANS
 CanvasThumbnail.propTypes = {
   imageUrl: PropTypes.string,
   isValid: PropTypes.bool,
-  height: PropTypes.number,
-  onClick: PropTypes.func.isRequired,
+  maxHeight: PropTypes.number,
+  maxWidth: PropTypes.number,
+  aspectRatio: PropTypes.number,
   style: PropTypes.object, // eslint-disable-line react/forbid-prop-types,
 };
 
 CanvasThumbnail.defaultProps = {
   imageUrl: null,
   isValid: true,
-  height: 150,
+  maxHeight: null,
+  maxWidth: null,
+  aspectRatio: null,
   style: {},
 };
diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js
index b79404d53926f62588a9ac732151838e2867959b..33674bdcde9cbd4d714da4b6963816052f2250e5 100644
--- a/src/components/ThumbnailNavigation.js
+++ b/src/components/ThumbnailNavigation.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
 import Grid from 'react-virtualized/dist/commonjs/Grid';
+import GridListTile from '@material-ui/core/GridListTile';
 import { CanvasThumbnail } from './CanvasThumbnail';
 import ManifestoCanvas from '../lib/ManifestoCanvas';
 import ns from '../config/css-ns';
@@ -66,18 +67,28 @@ export class ThumbnailNavigation extends Component {
           }}
           className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${columnIndex}`, this.currentCanvasClass(currentGroupings.map(canvas => canvas.index))])}
         >
-          {currentGroupings.map((canvas, i) => (
-            <div
-              key={canvas.index}
-              style={{ position: 'absolute', left: (style.width - 8) * i / 2, top: 2 }}
-            >
-              <CanvasThumbnail
+          {currentGroupings.map((canvas, i) => {
+            const { height } = config.thumbnailNavigation;
+            const manifestoCanvas = new ManifestoCanvas(canvas);
+
+            return (
+              <GridListTile
+                component="div"
+                key={canvas.index}
                 onClick={() => setCanvas(window.id, currentGroupings[0].index)}
-                imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)}
-                height={config.thumbnailNavigation.height}
-              />
-            </div>
-          ))}
+                style={{
+                  position: 'absolute', left: (style.width - 8) * i / 2, top: 2,
+                }}
+              >
+                <CanvasThumbnail
+                  imageUrl={manifestoCanvas.thumbnail(null, height)}
+                  maxHeight={config.thumbnailNavigation.height}
+                  maxWidth={style.width}
+                  aspectRatio={manifestoCanvas.aspectRatio}
+                />
+              </GridListTile>
+            );
+          })}
         </div>
       </div>
     );
diff --git a/src/components/ValidationCanvas.js b/src/components/ValidationCanvas.js
deleted file mode 100644
index dc3ed584ef790770d9f7887f516fa0abddad129c..0000000000000000000000000000000000000000
--- a/src/components/ValidationCanvas.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import ManifestoCanvas from '../lib/ManifestoCanvas';
-
-/**
- */
-export class ValidationCanvas extends ManifestoCanvas {
-  /**
-   * checks whether the canvas has a valid height
-   */
-  get hasValidHeight() {
-    return (
-      typeof this.canvas.getHeight() === 'number'
-      && this.canvas.getHeight() > 0
-    );
-  }
-
-  /**
-   * checks whether the canvas has a valid height
-   */
-  get hasValidWidth() {
-    return (
-      typeof this.canvas.getHeight() === 'number'
-      && this.canvas.getHeight() > 0
-    );
-  }
-
-  /**
-   * checks whether the canvas has valid dimensions
-   */
-  get hasValidDimensions() {
-    return (
-      this.hasValidHeight
-      && this.hasValidWidth
-    );
-  }
-}
diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js
index ed05c9d4a2b2ffe6c1820088d08d318dd70d51b0..f97cb053655e1deab8649869a59d0b57c2e3f103 100644
--- a/src/components/WindowSideBarCanvasPanel.js
+++ b/src/components/WindowSideBarCanvasPanel.js
@@ -5,20 +5,13 @@ import Typography from '@material-ui/core/Typography';
 import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import { CanvasThumbnail } from './CanvasThumbnail';
-import { ValidationCanvas } from './ValidationCanvas';
+import ManifestoCanvas from '../lib/ManifestoCanvas';
 import { getIdAndLabelOfCanvases } from '../state/selectors';
 
 /**
  * a panel showing the canvases for a given manifest
  */
 export class WindowSideBarCanvasPanel extends Component {
-  /**
-   * calculateScaledWidth - calculates the scaled width according to the given width and aspectRatio
-   */
-  static calculateScaledWidth(height, aspectRatio) {
-    return Math.floor(height * aspectRatio);
-  }
-
   /**
    * render
    */
@@ -35,32 +28,31 @@ export class WindowSideBarCanvasPanel extends Component {
         <List>
           {
             canvasesIdAndLabel.map((canvas, canvasIndex) => {
-              const validationCanvas = new ValidationCanvas(canvases[canvasIndex]);
-              const isValid = validationCanvas.hasValidDimensions;
+              const { width, height } = config.canvasNavigation;
+              const manifestoCanvas = new ManifestoCanvas(canvases[canvasIndex]);
+              const isValid = manifestoCanvas.hasValidDimensions;
               const onClick = () => { setCanvas(windowId, canvasIndex); }; // eslint-disable-line require-jsdoc, max-len
 
               return (
                 <ListItem
                   key={canvas.id}
+                  alignItems="flex-start"
+                  onClick={onClick}
+                  button
                 >
-                  <div>
+                  <div style={{ minWidth: 50 }}>
                     <CanvasThumbnail
                       className={classNames(classes.clickable)}
                       isValid={isValid}
-                      imageUrl={validationCanvas.thumbnail(config.canvasNavigation.height)}
-                      onClick={onClick}
-                      style={{
-                        cursor: 'pointer',
-                        height: config.canvasNavigation.height,
-                        width: isValid ? WindowSideBarCanvasPanel.calculateScaledWidth(config.canvasNavigation.height, validationCanvas.aspectRatio) : 'auto',
-                      }}
+                      imageUrl={manifestoCanvas.thumbnail(width, height)}
+                      maxHeight={config.canvasNavigation.height}
+                      maxWidth={config.canvasNavigation.width}
+                      aspectRatio={manifestoCanvas.aspectRatio}
                     />
                   </div>
                   <Typography
-                    className={classNames(classes.clickable, classes.label)}
-                    onClick={onClick}
+                    className={classNames(classes.label)}
                     variant="body2"
-                    color="secondary"
                   >
                     {canvas.label}
                   </Typography>
diff --git a/src/components/index.js b/src/components/index.js
index e624b5872b0f962af411342fe7ee3d2ecc50a4cb..72c49d7c4f66d2a1cb5f034b3f017f86e117c5b1 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -10,7 +10,6 @@ export * from './NestedMenu';
 export * from './OpenSeadragonViewer';
 export * from './SanitizedHtml';
 export * from './ThumbnailNavigation';
-export * from './ValidationCanvas';
 export * from './ViewerNavigation';
 export * from './Window';
 export * from './WindowList';
diff --git a/src/config/settings.js b/src/config/settings.js
index 24e7359328bd1a68dd5fc4d0b3d00a0d75fa6935..5cb87c448822c6eaa14ce75caa9c31eb786ae7a1 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -1,6 +1,7 @@
 export default {
   canvasNavigation: {
-    height: 100,
+    height: 50,
+    width: 50,
   },
   theme: { // Sets up a MaterialUI theme. See https://material-ui.com/customization/default-theme/
     palette: {
diff --git a/src/containers/WindowSideBarCanvasPanel.js b/src/containers/WindowSideBarCanvasPanel.js
index 7c821cc77aa2795f38a95c2cc7c21f82b313fc53..27432454fdceb75435187354de7eb35117fc377d 100644
--- a/src/containers/WindowSideBarCanvasPanel.js
+++ b/src/containers/WindowSideBarCanvasPanel.js
@@ -27,17 +27,12 @@ const mapDispatchToProps = { setCanvas: actions.setCanvas };
 /**
  *
  * @param theme
- * @returns {{clickable: {cursor: string},
- * label: {fontSize: string, paddingLeft: number}, windowSideBarH2: *}}
+ * @returns {label: {paddingLeft: number}, windowSideBarH2: *}}
  */
 const styles = theme => ({
   windowSideBarH2: theme.typography.h5,
-  clickable: {
-    cursor: 'pointer',
-  },
   label: {
-    fontSize: '8pt',
-    paddingLeft: 8,
+    paddingLeft: theme.spacing.unit,
   },
 });
 
diff --git a/src/lib/ManifestoCanvas.js b/src/lib/ManifestoCanvas.js
index 7952804d241b32b22dc11975ef9566b1c3ba56bd..885e080dfc3ed1ccae09f3c1a8258776d98ef51b 100644
--- a/src/lib/ManifestoCanvas.js
+++ b/src/lib/ManifestoCanvas.js
@@ -43,13 +43,70 @@ export default class ManifestoCanvas {
    * Creates a canonical image request for a thumb
    * @param {Number} height
    */
-  thumbnail(height = 150) {
-    const width = Math.floor(height * this.aspectRatio);
+  thumbnail(maxWidth = undefined, maxHeight = undefined) {
+    let width;
+    let height;
 
     if (!this.imageInformationUri) {
       return undefined;
     }
 
-    return this.canonicalImageUri.replace(/\/full\/.*\/0\//, `/full/${width},/0/`);
+    switch (this.thumbnailConstraints(maxWidth, maxHeight)) {
+      case 'sizeByH':
+        height = maxHeight;
+        break;
+      case 'sizeByW':
+        width = maxWidth;
+        break;
+      default:
+        height = '150';
+    }
+
+    // note that, although the IIIF server may support sizeByConfinedWh (e.g. !w,h)
+    // this is a IIIF level 2 feature, so we're instead providing w, or h,-style requests
+    // which are only level 1.
+    return this.canonicalImageUri.replace(/\/full\/.*\/0\//, `/full/${width || ''},${height || ''}/0/`);
+  }
+
+  /** @private */
+  thumbnailConstraints(maxWidth, maxHeight) {
+    if (!maxHeight && !maxWidth) return undefined;
+    if (maxHeight && !maxWidth) return 'sizeByH';
+    if (!maxHeight && maxWidth) return 'sizeByW';
+
+    const { aspectRatio } = this;
+    const desiredAspectRatio = maxWidth / maxHeight;
+
+    return desiredAspectRatio < aspectRatio ? 'sizeByW' : 'sizeByH';
+  }
+
+  /**
+   * checks whether the canvas has a valid height
+   */
+  get hasValidHeight() {
+    return (
+      typeof this.canvas.getHeight() === 'number'
+      && this.canvas.getHeight() > 0
+    );
+  }
+
+  /**
+   * checks whether the canvas has a valid height
+   */
+  get hasValidWidth() {
+    return (
+      typeof this.canvas.getHeight() === 'number'
+      && this.canvas.getHeight() > 0
+    );
+  }
+
+  /**
+   * checks whether the canvas has valid dimensions
+   */
+  get hasValidDimensions() {
+    return (
+      this.hasValidHeight
+      && this.hasValidWidth
+    );
   }
 }