diff --git a/__tests__/src/components/WindowViewSettings.test.js b/__tests__/src/components/WindowViewSettings.test.js
index 4a7c06a07d67cff892feadf14afaa1307ef279b0..bd19497b2e988c0282370fead6b29c0e778788d6 100644
--- a/__tests__/src/components/WindowViewSettings.test.js
+++ b/__tests__/src/components/WindowViewSettings.test.js
@@ -7,11 +7,12 @@ import { WindowViewSettings } from '../../../src/components/WindowViewSettings';
 
 /** create wrapper */
 function createWrapper(props) {
-  return shallow(
+  return mount(
     <WindowViewSettings
       classes={{}}
       windowId="xyz"
       setWindowViewType={() => {}}
+      viewTypes={['single', 'book', 'scroll', 'gallery']}
       windowViewType="single"
       {...props}
     />,
@@ -23,10 +24,11 @@ describe('WindowViewSettings', () => {
     const wrapper = createWrapper();
     expect(wrapper.find(ListSubheader).length).toBe(1);
     const labels = wrapper.find(FormControlLabel);
-    expect(labels.length).toBe(3);
+    expect(labels.length).toBe(4);
     expect(labels.at(0).props().value).toBe('single');
     expect(labels.at(1).props().value).toBe('book');
-    expect(labels.at(2).props().value).toBe('gallery');
+    expect(labels.at(2).props().value).toBe('scroll');
+    expect(labels.at(3).props().value).toBe('gallery');
   });
 
   it('should set the correct label active (by setting the secondary color)', () => {
@@ -37,8 +39,11 @@ describe('WindowViewSettings', () => {
     wrapper = createWrapper({ windowViewType: 'book' });
     expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).toEqual('secondary');
 
-    wrapper = createWrapper({ windowViewType: 'gallery' });
+    wrapper = createWrapper({ windowViewType: 'scroll' });
     expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).toEqual('secondary');
+
+    wrapper = createWrapper({ windowViewType: 'gallery' });
+    expect(wrapper.find(FormControlLabel).at(3).props().control.props.color).toEqual('secondary');
   });
 
   it('updates state when the view config selection changes', () => {
@@ -49,6 +54,8 @@ describe('WindowViewSettings', () => {
     wrapper.find(MenuItem).at(1).simulate('click');
     expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'book');
     wrapper.find(MenuItem).at(2).simulate('click');
+    expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'scroll');
+    wrapper.find(MenuItem).at(3).simulate('click');
     expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'gallery');
   });
 
@@ -58,6 +65,7 @@ describe('WindowViewSettings', () => {
         classes={{}}
         windowId="xyz"
         setWindowViewType={() => {}}
+        viewTypes={['single', 'book', 'scroll', 'gallery']}
         windowViewType="single"
       />,
     );
diff --git a/__tests__/src/lib/CanvasGroupings.test.js b/__tests__/src/lib/CanvasGroupings.test.js
index 1f222d299a75fe337e4315ac703eca70461ba370..2732be7b629a0af002c1a7535e6a2cc1b775539a 100644
--- a/__tests__/src/lib/CanvasGroupings.test.js
+++ b/__tests__/src/lib/CanvasGroupings.test.js
@@ -35,6 +35,16 @@ describe('CanvasGroupings', () => {
         expect(subject.groupings()[1]).toEqual([1, 2]);
       });
     });
+    describe('scroll', () => {
+      let subject;
+      beforeEach(() => {
+        subject = new CanvasGroupings([0, 1, 2, 3], 'scroll');
+      });
+      it('creates an array of all the canvases', () => {
+        expect(subject.groupings().length).toEqual(1);
+        expect(subject.groupings()[0]).toEqual([0, 1, 2, 3]);
+      });
+    });
   });
   describe('getCanvases', () => {
     describe('single', () => {
@@ -58,5 +68,11 @@ describe('CanvasGroupings', () => {
         expect(subject.getCanvases(2)).toEqual([2]);
       });
     });
+    describe('scroll', () => {
+      it('selects by index', () => {
+        const subject = new CanvasGroupings([0, 1, 2, 3], 'scroll');
+        expect(subject.getCanvases(0)).toEqual([0, 1, 2, 3]);
+      });
+    });
   });
 });
diff --git a/__tests__/src/lib/CanvasWorld.test.js b/__tests__/src/lib/CanvasWorld.test.js
index f30d00ed0184f123f07f70494df83a98afae7768..d010df137176bd2cd6aaa26e7988842ad1f08179 100644
--- a/__tests__/src/lib/CanvasWorld.test.js
+++ b/__tests__/src/lib/CanvasWorld.test.js
@@ -29,6 +29,14 @@ describe('CanvasWorld', () => {
       expect(new CanvasWorld(canvasSubset, null, 'right-to-left').contentResourceToWorldCoordinates({ id: 'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410/full/full/0/default.jpg' }))
         .toEqual([0, 0, 2848, 4288]);
     });
+    it('supports TTB orientations', () => {
+      expect(new CanvasWorld(canvasSubset, null, 'top-to-bottom').contentResourceToWorldCoordinates({ id: 'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410/full/full/0/default.jpg' }))
+        .toEqual([0, 1936, 2848, 4288]);
+    });
+    it('supports BTT orientations', () => {
+      expect(new CanvasWorld(canvasSubset, null, 'bottom-to-top').contentResourceToWorldCoordinates({ id: 'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410/full/full/0/default.jpg' }))
+        .toEqual([0, 0, 2848, 4288]);
+    });
     it('when placed by a fragment contains the offset', () => {
       const subject = new CanvasWorld(
         [Utils.parseManifest(fragmentFixture).getSequences()[0].getCanvases()[0]],
diff --git a/__tests__/src/selectors/windows.test.js b/__tests__/src/selectors/windows.test.js
index 5bc2699678b8a1b7162cd9e5c10583c73f4d6996..3db842886656c61a4c806408f48da6588de3aefd 100644
--- a/__tests__/src/selectors/windows.test.js
+++ b/__tests__/src/selectors/windows.test.js
@@ -11,6 +11,7 @@ import {
   getWindowManifests,
   getWindows,
   getMaximizedWindowsIds,
+  getAllowedWindowViewTypes,
 } from '../../../src/state/selectors/windows';
 
 describe('getWindows', () => {
@@ -104,6 +105,12 @@ describe('getWindowViewType', () => {
     config: {
       window: {
         defaultView: 'default',
+        views: [
+          { behaviors: ['individuals'], key: 'single' },
+          { behaviors: ['paged'], key: 'book' },
+          { behaviors: ['continuous'], key: 'scroll' },
+          { key: 'gallery' },
+        ],
       },
     },
     manifests: {
@@ -150,6 +157,36 @@ describe('getWindowViewType', () => {
   });
 });
 
+describe('getAllowedWindowViewTypes', () => {
+  const state = {
+    config: {
+      window: {
+        defaultView: 'single',
+        views: [
+          { behaviors: ['individuals'], key: 'single' },
+          { behaviors: ['paged'], key: 'book' },
+          { behaviors: ['continuous'], key: 'scroll' },
+          { key: 'gallery' },
+        ],
+      },
+    },
+    manifests: {
+      x: { json: { ...manifestFixture001 } },
+      y: { json: { ...manifestFixture015 } },
+    },
+  };
+
+  it('should return unrestricted view types', () => {
+    const received = getAllowedWindowViewTypes(state, { manifestId: 'x' });
+    expect(received).toEqual(['single', 'gallery']);
+  });
+
+  it('should return view types where behaviors match', () => {
+    const received = getAllowedWindowViewTypes(state, { manifestId: 'y' });
+    expect(received).toEqual(['single', 'book', 'gallery']);
+  });
+});
+
 describe('getViewer', () => {
   const state = {
     viewers: {
diff --git a/src/components/WindowViewSettings.js b/src/components/WindowViewSettings.js
index 1666c7a8a1fa28df0d2100d8971ea4db79d84e92..fdc05946517f5816668556e33c20315cdd6f743e 100644
--- a/src/components/WindowViewSettings.js
+++ b/src/components/WindowViewSettings.js
@@ -4,6 +4,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
 import MenuItem from '@material-ui/core/MenuItem';
 import ListSubheader from '@material-ui/core/ListSubheader';
 import SingleIcon from '@material-ui/icons/CropOriginalSharp';
+import ScrollViewIcon from '@material-ui/icons/ViewColumn';
 import PropTypes from 'prop-types';
 import BookViewIcon from './icons/BookViewIcon';
 import GalleryViewIcon from './icons/GalleryViewIcon';
@@ -55,44 +56,40 @@ export class WindowViewSettings extends Component {
    */
   render() {
     const {
-      classes, handleClose, t, windowViewType,
+      classes, handleClose, t, windowViewType, viewTypes,
     } = this.props;
 
+    const iconMap = {
+      book: BookViewIcon,
+      gallery: GalleryViewIcon,
+      scroll: ScrollViewIcon,
+      single: SingleIcon,
+    };
+
+    /** Suspiciously similar to a component, yet if it is invoked through JSX
+        none of the click handlers work? */
+    const menuItem = ({ value, Icon }) => (
+      <MenuItem
+        key={value}
+        className={classes.MenuItem}
+        ref={windowViewType === value && (ref => this.handleSelectedRef(ref))}
+        onClick={() => { this.handleChange(value); handleClose(); }}
+      >
+        <FormControlLabel
+          value={value}
+          classes={{ label: windowViewType === value ? classes.selectedLabel : classes.label }}
+          control={<Icon color={windowViewType === value ? 'secondary' : undefined} />}
+          label={t(value)}
+          labelPlacement="bottom"
+        />
+      </MenuItem>
+    );
+
+    if (viewTypes.length === 0) return null;
     return (
       <>
         <ListSubheader role="presentation" disableSticky tabIndex="-1">{t('view')}</ListSubheader>
-
-        <MenuItem
-          className={classes.MenuItem}
-          ref={ref => this.handleSelectedRef(ref)}
-          onClick={() => { this.handleChange('single'); handleClose(); }}
-        >
-          <FormControlLabel
-            value="single"
-            classes={{ label: windowViewType === 'single' ? classes.selectedLabel : classes.label }}
-            control={<SingleIcon color={windowViewType === 'single' ? 'secondary' : undefined} />}
-            label={t('single')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
-        <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('book'); handleClose(); }}>
-          <FormControlLabel
-            value="book"
-            classes={{ label: windowViewType === 'book' ? classes.selectedLabel : classes.label }}
-            control={<BookViewIcon color={windowViewType === 'book' ? 'secondary' : undefined} />}
-            label={t('book')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
-        <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('gallery'); handleClose(); }}>
-          <FormControlLabel
-            value="gallery"
-            classes={{ label: windowViewType === 'gallery' ? classes.selectedLabel : classes.label }}
-            control={<GalleryViewIcon color={windowViewType === 'gallery' ? 'secondary' : undefined} />}
-            label={t('gallery')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
+        { viewTypes.map(value => menuItem({ Icon: iconMap[value], value })) }
       </>
     );
   }
@@ -103,10 +100,12 @@ WindowViewSettings.propTypes = {
   handleClose: PropTypes.func,
   setWindowViewType: PropTypes.func.isRequired,
   t: PropTypes.func,
+  viewTypes: PropTypes.arrayOf(PropTypes.string),
   windowId: PropTypes.string.isRequired,
   windowViewType: PropTypes.string.isRequired,
 };
 WindowViewSettings.defaultProps = {
   handleClose: () => {},
   t: key => key,
+  viewTypes: [],
 };
diff --git a/src/config/settings.js b/src/config/settings.js
index a81460c6586aae9a6fdd51a1b2d5fdd9f58e82c2..419a0c62f6145753e59dd7c6a06cee1f53a26125 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -242,7 +242,13 @@ export default {
       canvas: true,
       annotations: true,
       search: true,
-    }
+    },
+    views: [
+      { key: 'single', behaviors: ['individuals'] },
+      { key: 'book', behaviors: ['paged'] },
+      { key: 'scroll', behaviors: ['continuous'] },
+      { key: 'gallery' },
+    ],
   },
   windows: [ // Array of windows to be open when mirador initializes (each object should at least provide a manifestId key with the value of the IIIF presentation manifest to load)
     /**
diff --git a/src/containers/WindowViewSettings.js b/src/containers/WindowViewSettings.js
index e4b47ae70797500b6eb463d415bbbd138a3914f6..6fc417f284b1f5b585c85ec369ddbd31946ece8a 100644
--- a/src/containers/WindowViewSettings.js
+++ b/src/containers/WindowViewSettings.js
@@ -4,7 +4,7 @@ import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
-import { getWindowViewType } from '../state/selectors';
+import { getAllowedWindowViewTypes, getWindowViewType } from '../state/selectors';
 import { WindowViewSettings } from '../components/WindowViewSettings';
 
 /**
@@ -21,6 +21,7 @@ const mapDispatchToProps = { setWindowViewType: actions.setWindowViewType };
  */
 const mapStateToProps = (state, { windowId }) => (
   {
+    viewTypes: getAllowedWindowViewTypes(state, { windowId }),
     windowViewType: getWindowViewType(state, { windowId }),
   }
 );
diff --git a/src/lib/CanvasGroupings.js b/src/lib/CanvasGroupings.js
index ab348f30740da53a7985fb45b9873373b3713a82..9939e8622ee089e105b5230c77f2818443cd0c7d 100644
--- a/src/lib/CanvasGroupings.js
+++ b/src/lib/CanvasGroupings.js
@@ -29,6 +29,9 @@ export default class CanvasGroupings {
     if (this._groupings) { // eslint-disable-line no-underscore-dangle
       return this._groupings; // eslint-disable-line no-underscore-dangle
     }
+    if (this.viewType === 'scroll') {
+      return [this.canvases];
+    }
     if (this.viewType !== 'book') {
       return this.canvases.map(canvas => [canvas]);
     }
diff --git a/src/lib/CanvasWorld.js b/src/lib/CanvasWorld.js
index 535f9fac351fdf387eae6e03d0fd079f4df845a4..f15f7d709f3ee655eb6d7fddd308788288a926ed 100644
--- a/src/lib/CanvasWorld.js
+++ b/src/lib/CanvasWorld.js
@@ -13,6 +13,7 @@ export default class CanvasWorld {
     this.canvases = canvases.map(c => new MiradorCanvas(c));
     this.layers = layers;
     this.viewingDirection = viewingDirection;
+    this._canvasDimensions = null; // eslint-disable-line no-underscore-dangle
   }
 
   /** */
@@ -20,65 +21,113 @@ export default class CanvasWorld {
     return this.canvases.map(canvas => canvas.id);
   }
 
+  /** */
+  get canvasDimensions() {
+    if (this._canvasDimensions) { // eslint-disable-line no-underscore-dangle
+      return this._canvasDimensions; // eslint-disable-line no-underscore-dangle
+    }
+
+    const [dirX, dirY] = this.canvasDirection;
+    const scale = dirY === 0
+      ? Math.min(...this.canvases.map(c => c.getHeight()))
+      : Math.min(...this.canvases.map(c => c.getWidth()));
+    let incX = 0;
+    let incY = 0;
+
+    const canvasDims = this.canvases.reduce((acc, canvas) => {
+      let canvasHeight;
+      let canvasWidth;
+
+      if (dirY === 0) {
+        // constant height
+        canvasHeight = scale;
+        canvasWidth = Math.floor(scale * canvas.aspectRatio);
+      } else {
+        // constant width
+        canvasWidth = scale;
+        canvasHeight = Math.floor(scale * (1 / canvas.aspectRatio));
+      }
+      acc.push({
+        canvas,
+        height: canvasHeight,
+        width: canvasWidth,
+        x: incX,
+        y: incY,
+      });
+
+      incX += dirX * canvasWidth;
+      incY += dirY * canvasHeight;
+      return acc;
+    }, []);
+
+    const worldHeight = dirY === 0 ? scale : Math.abs(incY);
+    const worldWidth = dirX === 0 ? scale : Math.abs(incX);
+
+    this._canvasDimensions = canvasDims // eslint-disable-line no-underscore-dangle
+      .reduce((acc, dims) => {
+        acc.push({
+          ...dims,
+          x: dirX === -1 ? dims.x + worldWidth - dims.width : dims.x,
+          y: dirY === -1 ? dims.y + worldHeight - dims.height : dims.y,
+        });
+
+        return acc;
+      }, []);
+
+    return this._canvasDimensions; // eslint-disable-line no-underscore-dangle
+  }
+
   /**
    * contentResourceToWorldCoordinates - calculates the contentResource coordinates
    * respective to the world.
    */
   contentResourceToWorldCoordinates(contentResource) {
-    const wholeBounds = this.worldBounds();
     const miradorCanvasIndex = this.canvases.findIndex(c => (
       c.imageResources.find(r => r.id === contentResource.id)
     ));
     const canvas = this.canvases[miradorCanvasIndex];
-    const scaledWidth = Math.floor(wholeBounds[3] * canvas.aspectRatio);
-    let x = 0;
-    if (miradorCanvasIndex === this.secondCanvasIndex) {
-      x = wholeBounds[2] - scaledWidth;
-    }
+    const [x, y, w, h] = this.canvasToWorldCoordinates(canvas.id);
+
     const fragmentOffset = canvas.onFragment(contentResource.id);
     if (fragmentOffset) {
       return [
         x + fragmentOffset[0],
-        0 + fragmentOffset[1],
+        y + fragmentOffset[1],
         fragmentOffset[2],
         fragmentOffset[3],
       ];
     }
     return [
       x,
-      0,
-      scaledWidth,
-      wholeBounds[3],
+      y,
+      w,
+      h,
     ];
   }
 
   /** */
   canvasToWorldCoordinates(canvasId) {
-    const wholeBounds = this.worldBounds();
-    const miradorCanvasIndex = this.canvases.findIndex(c => (c.id === canvasId));
-    const { aspectRatio } = this.canvases[miradorCanvasIndex];
-    const scaledWidth = Math.floor(wholeBounds[3] * aspectRatio);
-    let x = 0;
-    if (miradorCanvasIndex === this.secondCanvasIndex) {
-      x = wholeBounds[2] - scaledWidth;
-    }
+    const canvasDimensions = this.canvasDimensions.find(c => c.canvas.id === canvasId);
+
     return [
-      x,
-      0,
-      scaledWidth,
-      wholeBounds[3],
+      canvasDimensions.x,
+      canvasDimensions.y,
+      canvasDimensions.width,
+      canvasDimensions.height,
     ];
   }
 
-  /**
-   * secondCanvasIndex - index of the second canvas used for determining which
-   * is first
-   */
-  get secondCanvasIndex() {
-    return this.viewingDirection === 'right-to-left' ? 0 : 1;
+  /** */
+  get canvasDirection() {
+    switch (this.viewingDirection) {
+      case 'left-to-right': return [1, 0];
+      case 'right-to-left': return [-1, 0];
+      case 'top-to-bottom': return [0, 1];
+      case 'bottom-to-top': return [0, -1];
+      default: return [1, 0];
+    }
   }
 
-
   /** Get the IIIF content resource for an image */
   contentResource(infoResponseId) {
     const miradorCanvas = this.canvases.find(c => c.imageServiceIds.some(id => (
@@ -149,26 +198,14 @@ export default class CanvasWorld {
    * lined up horizontally starting from left to right.
    */
   worldBounds() {
-    const heights = [];
-    const dimensions = [];
-    this.canvases.forEach((canvas) => {
-      heights.push(canvas.getHeight());
-      dimensions.push({
-        height: canvas.getHeight(),
-        width: canvas.getWidth(),
-      });
-    });
-    const minHeight = Math.min(...heights);
-    let scaledWidth = 0;
-    dimensions.forEach((dim) => {
-      const aspectRatio = dim.width / dim.height;
-      scaledWidth += Math.floor(minHeight * aspectRatio);
-    });
+    const worldWidth = Math.max(...this.canvasDimensions.map(c => c.x + c.width));
+    const worldHeight = Math.max(...this.canvasDimensions.map(c => c.y + c.height));
+
     return [
       0,
       0,
-      scaledWidth,
-      minHeight,
+      worldWidth,
+      worldHeight,
     ];
   }
 }
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 08d855641731c9e190e329aea087c5160ffc3ca5..b655cb2d5347b049c8b0e8f3369b6fb335ffcc35 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -104,6 +104,7 @@
     "retry": "Retry",
     "right": "Right",
     "rights": "License",
+    "scroll": "Scroll",
     "searchInputLabel": "search terms",
     "searchNextResult": "Next result",
     "searchNoResults": "No results found",
diff --git a/src/state/selectors/config.js b/src/state/selectors/config.js
index 6612fceabb4b8442ed2b8f7e5d462040c6c5ef78..ed7981d2b42f61729f1b9dbdf0956255366d4d92 100644
--- a/src/state/selectors/config.js
+++ b/src/state/selectors/config.js
@@ -52,6 +52,11 @@ export const getDefaultView = createSelector(
   ({ window }) => window && window.defaultView,
 );
 
+export const getViewConfigs = createSelector(
+  [getConfig],
+  ({ window }) => (window && window.views) || [],
+);
+
 export const getThemeDirection = createSelector(
   [getConfig],
   ({ theme }) => theme.direction || 'ltr',
diff --git a/src/state/selectors/windows.js b/src/state/selectors/windows.js
index 4ffbe1422d892edc4cf24a0937c86548f9c5d5cf..c24ca52d8e9677a488879f893d9d8b2d3843723b 100644
--- a/src/state/selectors/windows.js
+++ b/src/state/selectors/windows.js
@@ -5,7 +5,7 @@ import {
   getManifestViewingHint,
   getManifestoInstance,
 } from './manifests';
-import { getDefaultView } from './config';
+import { getDefaultView, getViewConfigs } from './config';
 import { getWorkspaceType } from './workspace';
 
 /**
@@ -85,18 +85,41 @@ export const getWindowViewType = createSelector(
     getManifestViewingHint,
     getManifestBehaviors,
     getDefaultView,
+    getViewConfigs,
   ],
-  (window, manifestViewingHint, manifestBehaviors, defaultView) => {
-    const lookup = {
-      individuals: 'single',
-      paged: 'book',
-    };
-    return (window && window.view)
-      || lookup[manifestBehaviors.find(b => lookup[b]) || manifestViewingHint]
-      || defaultView;
+  (window, manifestViewingHint, manifestBehaviors, defaultView, viewConfig) => {
+    if (window && window.view) return window.view;
+
+    const config = viewConfig.find(view => (
+      view.behaviors
+      && view.behaviors.some(b => manifestViewingHint === b || manifestBehaviors.includes(b))
+    ));
+
+    return (config && config.key) || defaultView;
   },
 );
 
+/** */
+export const getAllowedWindowViewTypes = createSelector(
+  [
+    getManifestViewingHint,
+    getManifestBehaviors,
+    getDefaultView,
+    getViewConfigs,
+  ],
+  (manifestViewingHint, manifestBehaviors, defaultView, viewConfig) => (
+    viewConfig.reduce((allowedViews, view) => {
+      if (
+        view.key === defaultView
+        || !view.behaviors
+        || view.behaviors.some(b => (
+          manifestViewingHint === b || manifestBehaviors.includes(b)
+        ))) allowedViews.push(view.key);
+      return allowedViews;
+    }, [])
+  ),
+);
+
 export const getViewer = createSelector(
   [
     state => state.viewers,