diff --git a/__tests__/integration/mirador/thumbnail-navigation.test.js b/__tests__/integration/mirador/thumbnail-navigation.test.js
index 1baaa01d0b4d9b2e0e6a8329a2f52052c7e19c8e..840559c3503819e50db0d58e332b8fdbdaaeef0c 100644
--- a/__tests__/integration/mirador/thumbnail-navigation.test.js
+++ b/__tests__/integration/mirador/thumbnail-navigation.test.js
@@ -12,7 +12,7 @@ describe('Thumbnail navigation', () => {
     ));
     expect(Object.values(windows)[0].canvasIndex).toBe(2); // test harness in index.html starts at 2
     await page.waitFor(1000);
-    await expect(page).toClick('.mirador-thumbnail-nav-canvas-1');
+    await expect(page).toClick('.mirador-thumbnail-nav-canvas-1 img');
     await expect(page).toMatchElement('.mirador-thumbnail-nav-canvas-1.mirador-current-canvas', { timeout: 1500 });
     windows = await page.evaluate(() => (
       miradorInstance.store.getState().windows
diff --git a/__tests__/src/components/CanvasThumbnail.test.js b/__tests__/src/components/CanvasThumbnail.test.js
index b18d87ae3b2b51b69e2ba80226a0ad046239a9ff..afc3798f437e962d88a378765f20845352145434 100644
--- a/__tests__/src/components/CanvasThumbnail.test.js
+++ b/__tests__/src/components/CanvasThumbnail.test.js
@@ -16,11 +16,11 @@ describe('CanvasThumbnail', () => {
 
   it('renders properly', () => {
     expect(wrapper.matchesElement(
-      <div>
+      <>
         <IntersectionObserver onChange={wrapper.instance().handleIntersection}>
           <img alt="" />
         </IntersectionObserver>
-      </div>,
+      </>,
     )).toBe(true);
   });
 
diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js
index f493672e5b48b5aff9ccbc18c96be9c0a69551a7..f479471c387e4184b4ffbd53137d970777a30253 100644
--- a/__tests__/src/components/ThumbnailNavigation.test.js
+++ b/__tests__/src/components/ThumbnailNavigation.test.js
@@ -3,8 +3,27 @@ import { shallow } from 'enzyme';
 import Grid from 'react-virtualized/dist/commonjs/Grid';
 import manifesto from 'manifesto.js';
 import ThumbnailNavigation from '../../../src/components/ThumbnailNavigation';
+import CanvasGroupings from '../../../src/lib/CanvasGroupings';
 import manifestJson from '../../fixtures/version-2/019.json';
 
+/** create wrapper */
+function createWrapper(props) {
+  return shallow(
+    <ThumbnailNavigation
+      canvasGroupings={
+        new CanvasGroupings(manifesto.create(manifestJson).getSequences()[0].getCanvases())
+      }
+      window={{
+        id: 'foobar',
+        canvasIndex: 1,
+        thumbnailNavigationPosition: 'bottom',
+      }}
+      config={{ thumbnailNavigation: { height: 150 } }}
+      {...props}
+    />,
+  );
+}
+
 describe('ThumbnailNavigation', () => {
   let wrapper;
   let setCanvas;
@@ -17,20 +36,7 @@ describe('ThumbnailNavigation', () => {
     Grid.prototype._scrollingContainer = jest.fn( // eslint-disable-line no-underscore-dangle
       () => ({ scrollLeft: 0 }),
     );
-    wrapper = shallow(
-      <ThumbnailNavigation
-        canvases={
-          manifesto.create(manifestJson).getSequences()[0].getCanvases()
-        }
-        window={{
-          id: 'foobar',
-          canvasIndex: 1,
-          thumbnailNavigationPosition: 'bottom',
-        }}
-        config={{ thumbnailNavigation: { height: 150 } }}
-        setCanvas={setCanvas}
-      />,
-    );
+    wrapper = createWrapper({ setCanvas });
     grid = wrapper.find('AutoSizer')
       .dive()
       .find('Grid');
@@ -39,7 +45,7 @@ describe('ThumbnailNavigation', () => {
   it('renders the component', () => {
     expect(wrapper.find('.mirador-thumb-navigation').length).toBe(1);
   });
-  it('renders li elements based off of number of canvases', () => {
+  it('renders containers based off of number of canvases', () => {
     expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').length).toBe(3);
   });
   it('sets a mirador-current-canvas class on current canvas', () => {
@@ -59,6 +65,34 @@ describe('ThumbnailNavigation', () => {
   it('Grid is set with expected props for scrolling alignment', () => {
     expect(grid.props().scrollToAlignment).toBe('center');
     expect(grid.props().scrollToColumn).toBe(1);
-    expect(grid.props().columnIndex).toBe(1);
+  });
+  it('has a ref set used to reset on view change', () => {
+    expect(wrapper.instance().gridRef).not.toBe(null);
+  });
+  it('renders containers based off of canvas groupings ', () => {
+    wrapper = createWrapper({
+      setCanvas,
+      canvasGroupings: new CanvasGroupings(manifesto.create(manifestJson).getSequences()[0].getCanvases(), 'book'),
+    });
+    grid = wrapper.find('AutoSizer')
+      .dive()
+      .find('Grid');
+    renderedGrid = grid.dive();
+    expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').length).toBe(2);
+    expect(renderedGrid.find('CanvasThumbnail').length).toBe(3);
+    expect(wrapper.instance().scrollToColumn()).toBe(1);
+  });
+  it('triggers a recomputeGridSize on view change', () => {
+    const mockRecompute = jest.fn();
+    wrapper.instance().gridRef = { current: { recomputeGridSize: mockRecompute } };
+    wrapper.setProps({
+      window: {
+        id: 'foobar',
+        canvasIndex: 1,
+        thumbnailNavigationPosition: 'bottom',
+        view: 'book',
+      },
+    });
+    expect(mockRecompute).toHaveBeenCalled();
   });
 });
diff --git a/__tests__/src/components/WindowViewer.test.js b/__tests__/src/components/WindowViewer.test.js
index 14932afdd6fb191c99d6421d18ce7d7e0c5aae8b..0d0e21f59a45f7e7762dbc25b1fd0c436f0757b9 100644
--- a/__tests__/src/components/WindowViewer.test.js
+++ b/__tests__/src/components/WindowViewer.test.js
@@ -111,4 +111,14 @@ describe('WindowViewer', () => {
       });
     });
   });
+  it('when view type changes', () => {
+    expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(3);
+    wrapper.setProps({
+      window: {
+        canvasIndex: 0,
+        view: 'book',
+      },
+    });
+    expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(2);
+  });
 });
diff --git a/__tests__/src/lib/CanvasGroupings.test.js b/__tests__/src/lib/CanvasGroupings.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9cc6912535b669c7ff19b1dd52ab8cda1e29037
--- /dev/null
+++ b/__tests__/src/lib/CanvasGroupings.test.js
@@ -0,0 +1,56 @@
+import CanvasGroupings from '../../../src/lib/CanvasGroupings';
+
+describe('CanvasGroupings', () => {
+  describe('constructor', () => {
+    it('sets canvases and viewType', () => {
+      const subject = new CanvasGroupings([null, null], 'book');
+      expect(subject.viewType).toEqual('book');
+      expect(subject.canvases.length).toEqual(2);
+    });
+    it('viewType default is single', () => {
+      const subject = new CanvasGroupings([null, null]);
+      expect(subject.viewType).toEqual('single');
+    });
+  });
+  describe('groupings', () => {
+    describe('single', () => {
+      it('creates an array of arrays of the canvases', () => {
+        const subject = new CanvasGroupings([null, null, null, null]);
+        expect(subject.groupings().length).toEqual(4);
+        expect(subject.groupings()[0]).toEqual(expect.arrayContaining([null]));
+      });
+    });
+    describe('book', () => {
+      let subject;
+      beforeEach(() => {
+        subject = new CanvasGroupings([0, 1, 2, 3], 'book');
+      });
+      it('creates an array of groupings of the canvases', () => {
+        expect(subject.groupings().length).toEqual(3);
+      });
+      it('first grouping has only 1 canvas', () => {
+        expect(subject.groupings()[0]).toEqual([0]);
+      });
+      it('second grouping has 2 canvases', () => {
+        expect(subject.groupings()[1]).toEqual([1, 2]);
+      });
+    });
+  });
+  describe('getCanvases', () => {
+    describe('single', () => {
+      it('selects by index', () => {
+        const subject = new CanvasGroupings([0, 1, 2, 3]);
+        expect(subject.getCanvases(2)).toEqual([2]);
+      });
+    });
+    describe('book', () => {
+      let subject;
+      beforeEach(() => {
+        subject = new CanvasGroupings([0, 1, 2, 3], 'book');
+      });
+      it('selects by index / 2', () => {
+        expect(subject.getCanvases(2)).toEqual([1, 2]);
+      });
+    });
+  });
+});
diff --git a/src/components/CanvasThumbnail.js b/src/components/CanvasThumbnail.js
index b8a97d13ae96a521c5a4ee4ab25b0efde023ea2e..d60f95594be115934d99b9aac0222456c85294f6 100644
--- a/src/components/CanvasThumbnail.js
+++ b/src/components/CanvasThumbnail.js
@@ -40,7 +40,7 @@ export default class CanvasThumbnail extends Component {
     const { loaded, image } = this.state;
     const imgStyle = { height, width: '100%', ...style };
     return (
-      <div>
+      <>
         <IntersectionObserver onChange={this.handleIntersection}>
           <img
             alt=""
@@ -51,7 +51,7 @@ export default class CanvasThumbnail extends Component {
             style={imgStyle}
           />
         </IntersectionObserver>
-      </div>
+      </>
     );
   }
 }
diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js
index 686f38fa4c9c10981e0f05b4b5933204ec73f28f..468d8da8114bbb3b7e481fc39a6a04d2e4a9661c 100644
--- a/src/components/ThumbnailNavigation.js
+++ b/src/components/ThumbnailNavigation.js
@@ -17,21 +17,33 @@ class ThumbnailNavigation extends Component {
 
     this.cellRenderer = this.cellRenderer.bind(this);
     this.calculateScaledWidth = this.calculateScaledWidth.bind(this);
+    this.gridRef = React.createRef();
+  }
+
+  /**
+   * If the view has changed and the thumbnailNavigation is open, recompute all
+   * of the grids
+   */
+  componentDidUpdate(prevProps) {
+    const { window } = this.props;
+    if (prevProps.window.view !== window.view && window.thumbnailNavigationPosition !== 'off') {
+      this.gridRef.current.recomputeGridSize();
+    }
   }
 
   /**
    * Determines whether the current index is the rendered canvas, providing
    * a useful class.
    */
-  currentCanvasClass(canvasIndex) {
+  currentCanvasClass(canvasIndices) {
     const { window } = this.props;
-    if (window.canvasIndex === canvasIndex) return 'current-canvas';
+    if (canvasIndices.includes(window.canvasIndex)) return 'current-canvas';
     return '';
   }
 
   /**
-   * Renders a given "cell" for a react-virtualized Grid. Right now this is a
-   * "canvas" but in the future for paged items, would be connected canvases.
+   * Renders a given "cell" for a react-virtualized Grid. This is a grouping of
+   * canvases.
    * https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md
    */
   cellRenderer(options) {
@@ -39,9 +51,9 @@ class ThumbnailNavigation extends Component {
       columnIndex, key, style,
     } = options;
     const {
-      window, setCanvas, config, canvases,
+      window, setCanvas, config, canvasGroupings,
     } = this.props;
-    const canvas = canvases[columnIndex];
+    const currentGroupings = canvasGroupings.groupings()[columnIndex];
     return (
       <div
         key={key}
@@ -52,13 +64,20 @@ class ThumbnailNavigation extends Component {
           style={{
             width: style.width - 8,
           }}
-          className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${canvas.index}`, this.currentCanvasClass(canvas.index)])}
+          className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${columnIndex}`, this.currentCanvasClass(currentGroupings.map(canvas => canvas.index))])}
         >
-          <CanvasThumbnail
-            onClick={() => setCanvas(window.id, canvas.index)}
-            imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)}
-            height={config.thumbnailNavigation.height}
-          />
+          {currentGroupings.map((canvas, i) => (
+            <div
+              key={canvas.index}
+              style={{ position: 'absolute', left: (style.width - 8) * i / 2, top: 2 }}
+            >
+              <CanvasThumbnail
+                onClick={() => setCanvas(window.id, currentGroupings[0].index)}
+                imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)}
+                height={config.thumbnailNavigation.height}
+              />
+            </div>
+          ))}
         </div>
       </div>
     );
@@ -66,19 +85,36 @@ class ThumbnailNavigation extends Component {
 
   /**
    * calculateScaledWidth - calculates the scaled width of a column for a Grid
-   * in this simple case, a column == canvas.
+   * in a simple case, a column == canvas. In a book view, a group (or two)
+   * canvases
    */
   calculateScaledWidth(options) {
-    const { config, canvases } = this.props;
-    const canvas = new ManifestoCanvas(canvases[options.index]);
-    return Math.floor(config.thumbnailNavigation.height * canvas.aspectRatio) + 8;
+    const { config, canvasGroupings } = this.props;
+    return canvasGroupings
+      .getCanvases(options.index)
+      .map(canvas => new ManifestoCanvas(canvas).aspectRatio)
+      .reduce((acc, current) => acc + Math.floor(config.thumbnailNavigation.height * current), 8);
+  }
+
+  /**
+   * In book view, this is halved to represent the proxy between the "canvasIndex"
+   * and the columnIndex (in this case the index of grouped canvases)
+   */
+  scrollToColumn() {
+    const { window } = this.props;
+    switch (window.view) {
+      case 'book':
+        return Math.ceil(window.canvasIndex / 2);
+      default:
+        return window.canvasIndex;
+    }
   }
 
   /**
    * Renders things
    */
   render() {
-    const { config, window, canvases } = this.props;
+    const { config, window, canvasGroupings } = this.props;
     if (window.thumbnailNavigationPosition === 'off') {
       return <></>;
     }
@@ -94,15 +130,15 @@ class ThumbnailNavigation extends Component {
           {({ height, width }) => (
             <Grid
               cellRenderer={this.cellRenderer}
-              columnCount={canvases.length}
-              columnIndex={window.canvasIndex}
+              columnCount={canvasGroupings.groupings().length}
               columnWidth={this.calculateScaledWidth}
               height={config.thumbnailNavigation.height}
               rowCount={1}
               rowHeight={config.thumbnailNavigation.height}
               scrollToAlignment="center"
-              scrollToColumn={window.canvasIndex}
+              scrollToColumn={this.scrollToColumn()}
               width={width}
+              ref={this.gridRef}
             />
           )}
         </AutoSizer>
@@ -113,7 +149,7 @@ class ThumbnailNavigation extends Component {
 
 ThumbnailNavigation.propTypes = {
   config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
-  canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
+  canvasGroupings: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   setCanvas: PropTypes.func.isRequired,
   window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
 };
diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js
index 2e752032eef19ead37dcef8b4aa16264bb090d81..e0e241f5c9a009d7b37039675fa7ec5b0b6f1288 100644
--- a/src/components/WindowSideBarCanvasPanel.js
+++ b/src/components/WindowSideBarCanvasPanel.js
@@ -44,17 +44,19 @@ class WindowSideBarCanvasPanel extends Component {
                 <ListItem
                   key={canvas.id}
                 >
-                  <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',
-                    }}
-                  />
+                  <div>
+                    <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',
+                      }}
+                    />
+                  </div>
                   <Typography
                     className={classNames(classes.clickable, classes.label)}
                     onClick={onClick}
diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js
index 4f8264cbcef8501166a5ae4e937b8cfc1b6c27a7..b833a101f80a7816e4c388f049388036a91b07a8 100644
--- a/src/components/WindowViewer.js
+++ b/src/components/WindowViewer.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import OSDViewer from '../containers/OpenSeadragonViewer';
 import ViewerNavigation from '../containers/ViewerNavigation';
 import ManifestoCanvas from '../lib/ManifestoCanvas';
+import CanvasGroupings from '../lib/CanvasGroupings';
 
 /**
  * Represents a WindowViewer in the mirador workspace. Responsible for mounting
@@ -15,8 +16,9 @@ class WindowViewer extends Component {
   constructor(props) {
     super(props);
 
-    const { manifest } = this.props;
+    const { manifest, window } = this.props;
     this.canvases = manifest.manifestation.getSequences()[0].getCanvases();
+    this.canvasGroupings = new CanvasGroupings(this.canvases, window.view);
   }
 
   /**
@@ -46,6 +48,10 @@ class WindowViewer extends Component {
         fetchInfoResponse(new ManifestoCanvas(canvas).imageInformationUri);
       });
     }
+    // If the view changes, create a new instance
+    if (prevProps.window.view !== window.view) {
+      this.canvasGroupings = new CanvasGroupings(this.canvases, window.view);
+    }
   }
 
   /**
@@ -62,28 +68,12 @@ class WindowViewer extends Component {
   }
 
   /**
-   * Figures out how many and what canvases to present to a user based off of
-   * the view, number of canvases, and canvasIndex.
+   * Uses CanvasGroupings to figure out how many and what canvases to present to
+   * a user based off of the view, number of canvases, and canvasIndex.
    */
   currentCanvases() {
     const { window } = this.props;
-    switch (window.view) {
-      case 'book':
-        if ( // FIXME: There is probably better logic floating around out there to determine this.
-          this.canvases.length > 0 // when there are canvases present
-          && window.canvasIndex !== 0 // when the first canvas is not selected
-          && window.canvasIndex + 1 !== this.canvases.length // when the last canvas is not selected
-        ) {
-          // For an even canvas
-          if (window.canvasIndex % 2 === 0) {
-            return [this.canvases[window.canvasIndex - 1], this.canvases[window.canvasIndex]];
-          }
-          return [this.canvases[window.canvasIndex], this.canvases[window.canvasIndex + 1]];
-        }
-        return [this.canvases[window.canvasIndex]];
-      default:
-        return [this.canvases[window.canvasIndex]];
-    }
+    return this.canvasGroupings.getCanvases(window.canvasIndex);
   }
 
   /**
diff --git a/src/containers/ThumbnailNavigation.js b/src/containers/ThumbnailNavigation.js
index 0f7b5658f41d6a193347fe3cc898b18d54668eb4..d0e33828d6f268dffd4363b66835f0df73212bff 100644
--- a/src/containers/ThumbnailNavigation.js
+++ b/src/containers/ThumbnailNavigation.js
@@ -1,6 +1,7 @@
 import { compose } from 'redux';
 import { connect } from 'react-redux';
 import miradorWithPlugins from '../lib/miradorWithPlugins';
+import CanvasGroupings from '../lib/CanvasGroupings';
 import * as actions from '../state/actions';
 import ThumbnailNavigation from '../components/ThumbnailNavigation';
 import { getManifestCanvases } from '../state/selectors';
@@ -9,8 +10,8 @@ import { getManifestCanvases } from '../state/selectors';
  * @memberof ThumbnailNavigation
  * @private
  */
-const mapStateToProps = ({ config }, { manifest }) => ({
-  canvases: getManifestCanvases(manifest),
+const mapStateToProps = ({ config }, { manifest, window }) => ({
+  canvasGroupings: new CanvasGroupings(getManifestCanvases(manifest), window.view),
   config,
 });
 
diff --git a/src/lib/CanvasGroupings.js b/src/lib/CanvasGroupings.js
new file mode 100644
index 0000000000000000000000000000000000000000..45438867bcc4d1f569808f5a22322a16d3346329
--- /dev/null
+++ b/src/lib/CanvasGroupings.js
@@ -0,0 +1,52 @@
+/**
+ *
+ */
+export default class CanvasGroupings {
+  /**
+   */
+  constructor(canvases, viewType = 'single') {
+    this.canvases = canvases;
+    this.viewType = viewType;
+    this._groupings = null; // eslint-disable-line no-underscore-dangle
+  }
+
+  /**
+   */
+  getCanvases(index) {
+    switch (this.viewType) {
+      case 'book':
+        return this.groupings()[Math.ceil(index / 2)];
+      default:
+        return this.groupings()[index];
+    }
+  }
+
+  /**
+   * Groups a set of canvases based on the view type. Single, is just an array
+   * of canvases, while book view creates pairs.
+   */
+  groupings() {
+    if (this._groupings) { // eslint-disable-line no-underscore-dangle
+      return this._groupings; // eslint-disable-line no-underscore-dangle
+    }
+    if (this.viewType === 'single') {
+      return this.canvases.map(canvas => [canvas]);
+    }
+    const groupings = [];
+    this.canvases.forEach((canvas, i) => {
+      if (i === 0) {
+        groupings.push([canvas]);
+        return;
+      }
+      // Odd page
+      if (i % 2 !== 0) {
+        groupings.push([canvas]);
+      } else {
+        // Even page
+        groupings[Math.ceil(i / 2)].push(canvas);
+      }
+    });
+    this._groupings = groupings; // eslint-disable-line no-underscore-dangle
+    return groupings;
+  }
+}