diff --git a/__tests__/integration/mirador/thumbnail-navigation.test.js b/__tests__/integration/mirador/thumbnail-navigation.test.js
index 4056d5453028fe039f962622b79dff42553300dc..de83c2d2d0a09bb727e7b9a8d3b51be6f029cf3a 100644
--- a/__tests__/integration/mirador/thumbnail-navigation.test.js
+++ b/__tests__/integration/mirador/thumbnail-navigation.test.js
@@ -3,13 +3,6 @@
 describe('Thumbnail navigation', () => {
   beforeAll(async () => {
     await page.goto('http://127.0.0.1:4488/__tests__/integration/mirador/');
-    await expect(page).toClick('#addBtn');
-    await expect(page).toFill('#manifestURL', 'http://localhost:5000/api/019');
-    await expect(page).toClick('#fetchBtn');
-    // TODO: Refactor the app so we get rid of the wait
-    await page.waitFor(1000);
-    await expect(page).toClick('.mirador-manifest-list-item');
-    await page.waitFor(1000);
   });
 
   it('navigates a manifest using thumbnail navigation', async () => {
diff --git a/__tests__/src/actions/workspace.test.js b/__tests__/src/actions/workspace.test.js
index 9f9b3ff08a4278ba87188163d52b43814e75a297..b8e74464fc08637f0588007cd52282828d1cfb29 100644
--- a/__tests__/src/actions/workspace.test.js
+++ b/__tests__/src/actions/workspace.test.js
@@ -13,4 +13,15 @@ describe('workspace actions', () => {
       expect(actions.fullscreenWorkspace(options)).toEqual(expectedAction);
     });
   });
+  describe('updateWorkspaceMosaicLayout', () => {
+    it('should updates mosaic layout', () => {
+      const options = { foo: 'bar' };
+
+      const expectedAction = {
+        type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT,
+        layout: { foo: 'bar' },
+      };
+      expect(actions.updateWorkspaceMosaicLayout(options)).toEqual(expectedAction);
+    });
+  });
 });
diff --git a/__tests__/src/components/Window.test.js b/__tests__/src/components/Window.test.js
index f0f7ffdbbe750b08ee93beb4a5f350269fb33d13..0314462641216de887746ecfc17daed0d0d60ffb 100644
--- a/__tests__/src/components/Window.test.js
+++ b/__tests__/src/components/Window.test.js
@@ -10,8 +10,6 @@ describe('Window', () => {
   it('should render outer element', () => {
     wrapper = shallow(<Window window={window} />);
     expect(wrapper.find('.mirador-window')).toHaveLength(1);
-    expect(wrapper.instance().styleAttributes())
-      .toEqual({ width: '400px', height: '500px' });
   });
   it('should render <WindowTopBar>', () => {
     wrapper = shallow(<Window window={window} />);
diff --git a/__tests__/src/components/Workspace.test.js b/__tests__/src/components/Workspace.test.js
index 18774065fab1ce64e7b0419cf02011a6eca60382..138523c2b169aa9f09efe8d0b523a58622f37368 100644
--- a/__tests__/src/components/Workspace.test.js
+++ b/__tests__/src/components/Workspace.test.js
@@ -5,13 +5,36 @@ import Window from '../../../src/containers/Window';
 
 describe('Workspace', () => {
   const windows = { 1: { id: 1 }, 2: { id: 2 } };
+  let wrapper;
+  beforeEach(() => {
+    wrapper = shallow(
+      <Workspace
+        windows={windows}
+        config={{ workspace: { type: 'mosaic' } }}
+      />,
+    );
+  });
   it('should render properly', () => {
-    const wrapper = shallow(<Workspace windows={windows} />);
-    expect(wrapper.matchesElement(
-      <div className="mirador-workspace">
-        <Window window={{ id: 1 }} />
-        <Window window={{ id: 2 }} />
-      </div>,
-    )).toBe(true);
+    expect(wrapper.find('.mirador-workspace').length).toBe(1);
+    expect(wrapper.find('Connect(WorkspaceMosaic)').length).toBe(1);
+  });
+  describe('workspaceByType', () => {
+    it('when mosaic', () => {
+      expect(wrapper.find('Connect(WorkspaceMosaic)').length).toBe(1);
+    });
+    it('anything else', () => {
+      wrapper = shallow(
+        <Workspace
+          windows={windows}
+          config={{ workspace: { type: 'foo' } }}
+        />,
+      );
+      expect(wrapper.matchesElement(
+        <div className="mirador-workspace">
+          <Window window={{ id: 1 }} />
+          <Window window={{ id: 2 }} />
+        </div>,
+      )).toBe(true);
+    });
   });
 });
diff --git a/__tests__/src/components/WorkspaceMosaic.test.js b/__tests__/src/components/WorkspaceMosaic.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..64b2a013163298979de207e54edeeae2540525ea
--- /dev/null
+++ b/__tests__/src/components/WorkspaceMosaic.test.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { Mosaic } from 'react-mosaic-component';
+import WorkspaceMosaic from '../../../src/components/WorkspaceMosaic';
+
+describe('WorkspaceMosaic', () => {
+  const windows = { 1: { id: 1 }, 2: { id: 2 } };
+  let wrapper;
+  beforeEach(() => {
+    wrapper = shallow(
+      <WorkspaceMosaic
+        windows={windows}
+        workspace={{}}
+        updateWorkspaceMosaicLayout={() => {}}
+      />,
+    );
+  });
+  it('should render properly with an initialValue', () => {
+    expect(wrapper.matchesElement(
+      <Mosaic initialValue={{ direction: 'row', first: '1', second: '2' }} />,
+    )).toBe(true);
+  });
+  describe('determineWorkspaceLayout', () => {
+    it('when window ids do not match workspace layout', () => {
+      wrapper = shallow(
+        <WorkspaceMosaic
+          windows={windows}
+          workspace={{ layout: 'foo' }}
+          updateWorkspaceMosaicLayout={() => {}}
+        />,
+      );
+      expect(wrapper.instance().determineWorkspaceLayout()).toMatchObject({
+        direction: 'row', first: '1', second: '2',
+      });
+    });
+    it('when window ids match workspace layout', () => {
+      wrapper = shallow(
+        <WorkspaceMosaic
+          windows={{ foo: { id: 'foo' } }}
+          workspace={{ layout: 'foo' }}
+          updateWorkspaceMosaicLayout={() => {}}
+        />,
+      );
+      expect(wrapper.instance().determineWorkspaceLayout()).toBeNull();
+    });
+  });
+  describe('tileRenderer', () => {
+    it('when window is available', () => {
+      expect(wrapper.instance().tileRenderer('1')).not.toBeNull();
+    });
+    it('when window is not available', () => {
+      expect(wrapper.instance().tileRenderer('bar')).toBeNull();
+    });
+  });
+  describe('mosaicChange', () => {
+    it('calls the provided prop to update layout', () => {
+      const mock = jest.fn();
+      wrapper = shallow(
+        <WorkspaceMosaic
+          windows={{ foo: { id: 'foo' } }}
+          workspace={{ layout: 'foo' }}
+          updateWorkspaceMosaicLayout={mock}
+        />,
+      );
+      wrapper.instance().mosaicChange();
+      expect(mock).toBeCalled();
+    });
+  });
+});
diff --git a/__tests__/src/reducers/workspace.test.js b/__tests__/src/reducers/workspace.test.js
index 756a2990268ec9ee0822086128367cd9e9c113e2..7f80dab51600d2665081e21bb1c895e5c79e7dd4 100644
--- a/__tests__/src/reducers/workspace.test.js
+++ b/__tests__/src/reducers/workspace.test.js
@@ -18,4 +18,12 @@ describe('workspace reducer', () => {
       fullscreen: true,
     });
   });
+  it('should handle UPDATE_WORKSPACE_MOSAIC_LAYOUT', () => {
+    expect(reducer([], {
+      type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT,
+      layout: { foo: 'bar' },
+    })).toEqual({
+      layout: { foo: 'bar' },
+    });
+  });
 });
diff --git a/package.json b/package.json
index ef8a0f9f3227206619e24555677993f4813c92f5..d147f892767a6bc609542207aa5e4a093cc8f04b 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "react": "^16.7.0",
     "react-dom": "^16.4.0",
     "react-fullscreen-crossbrowser": "^1.0.9",
+    "react-mosaic-component": "^2.0.2",
     "react-redux": "^6.0.0",
     "react-virtualized": "^9.21.0",
     "redux": "4.0.1",
diff --git a/src/components/Window.js b/src/components/Window.js
index 91e83bfc6444f9a2e47d456d0957974d596bb917..2d1fec5604b0aa076fd570c83bd5777eb63d7f24 100644
--- a/src/components/Window.js
+++ b/src/components/Window.js
@@ -10,21 +10,13 @@ import ThumbnailNavigation from '../containers/ThumbnailNavigation';
  * @param {object} window
  */
 class Window extends Component {
-  /**
-   * Return style attributes
-   */
-  styleAttributes() {
-    const { window } = this.props;
-    return { width: `${window.xywh[2]}px`, height: `${window.xywh[3]}px` };
-  }
-
   /**
    * Renders things
    */
   render() {
     const { manifest, window } = this.props;
     return (
-      <div className={ns('window')} style={this.styleAttributes()}>
+      <div className={ns('window')}>
         <WindowTopBar
           windowId={window.id}
           manifest={manifest}
diff --git a/src/components/Workspace.js b/src/components/Workspace.js
index 9f3244339b0ca9b98f94b480169159b09ae673ac..35cef899bf2e6092c1b19064a4a243cc30fa9f72 100644
--- a/src/components/Workspace.js
+++ b/src/components/Workspace.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import Window from '../containers/Window';
+import WorkspaceMosaic from '../containers/WorkspaceMosaic';
 import ns from '../config/css-ns';
 
 /**
@@ -9,21 +10,39 @@ import ns from '../config/css-ns';
  * @private
  */
 class Workspace extends React.Component {
+  /**
+   */
+  constructor(props) {
+    super(props);
+
+    this.workspaceByType = this.workspaceByType.bind(this);
+  }
+
+  /**
+   * Determine which workspace to render by configured type
+   */
+  workspaceByType() {
+    const { config, windows } = this.props;
+    switch (config.workspace.type) {
+      case 'mosaic':
+        return <WorkspaceMosaic windows={windows} />;
+      default:
+        return Object.values(windows).map(window => (
+          <Window
+            key={window.id}
+            window={window}
+          />
+        ));
+    }
+  }
+
   /**
    * render
    */
   render() {
-    const { windows } = this.props;
     return (
       <div className={ns('workspace')}>
-        {
-          Object.values(windows).map(window => (
-            <Window
-              key={window.id}
-              window={window}
-            />
-          ))
-        }
+        {this.workspaceByType()}
       </div>
     );
   }
@@ -31,6 +50,7 @@ class Workspace extends React.Component {
 
 Workspace.propTypes = {
   windows: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+  config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
 };
 
 export default Workspace;
diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a7b0b0eb6326d0a3c581bfbf5f10244a280fe9b
--- /dev/null
+++ b/src/components/WorkspaceMosaic.js
@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+  Mosaic, getLeaves, createBalancedTreeFromLeaves,
+} from 'react-mosaic-component';
+import 'react-mosaic-component/react-mosaic-component.css';
+import Window from '../containers/Window';
+
+/**
+ * Represents a work area that contains any number of windows
+ * @memberof Workspace
+ * @private
+ */
+class WorkspaceMosaic extends React.Component {
+  /**
+   */
+  constructor(props) {
+    super(props);
+
+    this.tileRenderer = this.tileRenderer.bind(this);
+    this.mosaicChange = this.mosaicChange.bind(this);
+    this.determineWorkspaceLayout = this.determineWorkspaceLayout.bind(this);
+  }
+
+  /**
+   * Render a tile (Window) in the Mosaic.
+   */
+  tileRenderer(id, path) {
+    const { windows } = this.props;
+    const window = windows[id];
+    if (!window) return null;
+    return (
+      <Window
+        key={window.id}
+        window={window}
+      />
+    );
+  }
+
+  /**
+   * Update the redux store when the Mosaic is changed.
+   */
+  mosaicChange(newLayout) {
+    const { updateWorkspaceMosaicLayout } = this.props;
+    updateWorkspaceMosaicLayout(newLayout);
+  }
+
+  /**
+   * Used to determine whether or not a "new" layout should be autogenerated.
+   * If a Window is added or removed, generate that new layout and use that for
+   * this render. When the Mosaic changes, that will trigger a new store update.
+   */
+  determineWorkspaceLayout() {
+    const { windows, workspace } = this.props;
+    const windowKeys = Object.keys(windows);
+    const leaveKeys = getLeaves(workspace.layout);
+    // Check every window is in the layout, and all layout windows are present
+    // in store
+    if (!windowKeys.every(e => leaveKeys.includes(e))
+    || !leaveKeys.every(e => windowKeys.includes(e))) {
+      const newLayout = createBalancedTreeFromLeaves(windowKeys);
+      return newLayout;
+    }
+    return null;
+  }
+
+  /**
+   */
+  render() {
+    const { workspace } = this.props;
+    const newLayout = this.determineWorkspaceLayout();
+    return (
+      <Mosaic
+        renderTile={this.tileRenderer}
+        initialValue={newLayout || workspace.layout}
+        onChange={this.mosaicChange}
+        className="mirador-mosaic"
+        zeroStateView={<div />}
+      />
+    );
+  }
+}
+
+
+WorkspaceMosaic.propTypes = {
+  updateWorkspaceMosaicLayout: PropTypes.func.isRequired,
+  windows: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+  workspace: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+};
+
+export default WorkspaceMosaic;
diff --git a/src/config/settings.js b/src/config/settings.js
index 258339b2c14090a3b0fb18690ae23cc00f5821cb..fe3312aa23bd651b817acb62eb2f9f24c60954a1 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -4,4 +4,7 @@ export default {
     defaultPosition: 'bottom',
     height: 150,
   },
+  workspace: {
+    type: 'mosaic',
+  }
 };
diff --git a/src/containers/Window.js b/src/containers/Window.js
index 600c03d270346415751f805389a3f8b7fef64135..1532051f8aaec48f74001fe920697c10852aca77 100644
--- a/src/containers/Window.js
+++ b/src/containers/Window.js
@@ -7,8 +7,9 @@ import Window from '../components/Window';
  * @memberof Window
  * @private
  */
-const mapStateToProps = ({ manifests }, props) => ({
+const mapStateToProps = ({ manifests, windows }, props) => ({
   manifest: manifests[props.window.manifestId],
+  window: windows[props.window.id],
 });
 
 const enhance = compose(
diff --git a/src/containers/Workspace.js b/src/containers/Workspace.js
index ba8a411fb18a6774c3886d3b6bc21850210faacf..0bac3285eb2f9364e1f08423dd0dcad13d6013a9 100644
--- a/src/containers/Workspace.js
+++ b/src/containers/Workspace.js
@@ -1,5 +1,6 @@
 import { compose } from 'redux';
 import { connect } from 'react-redux';
+import * as actions from '../state/actions';
 import Workspace from '../components/Workspace';
 
 /**
@@ -9,6 +10,7 @@ import Workspace from '../components/Workspace';
  */
 const mapStateToProps = state => (
   {
+    config: state.config,
     windows: state.windows,
   }
 );
diff --git a/src/containers/WorkspaceMosaic.js b/src/containers/WorkspaceMosaic.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6b4a388caf5c5551936a146aabc5d6f0a95b2da
--- /dev/null
+++ b/src/containers/WorkspaceMosaic.js
@@ -0,0 +1,30 @@
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import * as actions from '../state/actions';
+import WorkspaceMosaic from '../components/WorkspaceMosaic';
+
+/**
+ * mapStateToProps - to hook up connect
+ * @memberof Workspace
+ * @private
+ */
+const mapStateToProps = state => (
+  {
+    workspace: state.workspace,
+  }
+);
+
+
+/**
+ * mapDispatchToProps - used to hook up connect to action creators
+ * @memberof Workspace
+ * @private
+ */
+const mapDispatchToProps = { updateWorkspaceMosaicLayout: actions.updateWorkspaceMosaicLayout };
+
+const enhance = compose(
+  connect(mapStateToProps, mapDispatchToProps),
+  // further HOC go here
+);
+
+export default enhance(WorkspaceMosaic);
diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js
index c5e503f91bb105f1f5742c53af2f8eaa7f0f8da8..302d40515fb883623162514d066515d585fe7a89 100644
--- a/src/state/actions/action-types.js
+++ b/src/state/actions/action-types.js
@@ -20,6 +20,7 @@ const ActionTypes = {
   RECEIVE_INFO_RESPONSE: 'RECEIVE_INFO_RESPONSE',
   RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
   REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
+  UPDATE_WORKSPACE_MOSAIC_LAYOUT: 'UPDATE_WORKSPACE_MOSAIC_LAYOUT',
 };
 
 export default ActionTypes;
diff --git a/src/state/actions/workspace.js b/src/state/actions/workspace.js
index 84ea9a0dc0f9db2afb248b73ef187f7987ee98bb..331ff96d3bccad0c6089875f90f2c2ba3c4aec1f 100644
--- a/src/state/actions/workspace.js
+++ b/src/state/actions/workspace.js
@@ -10,3 +10,13 @@ import ActionTypes from './action-types';
 export function fullscreenWorkspace(fullscreen) {
   return { type: ActionTypes.FULLSCREEN_WORKSPACE, fullscreen };
 }
+
+/**
+ * updateWorkspaceMosaicLayout - action creator
+ *
+ * @param  {Object} layout
+ * @memberof ActionCreators
+ */
+export function updateWorkspaceMosaicLayout(layout) {
+  return { type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT, layout };
+}
diff --git a/src/state/reducers/workspace.js b/src/state/reducers/workspace.js
index 874ea83f7edbfad96525567df91e912bd59f0fde..32d2b33de1335fcba4b68922bd8aa3b4b6ccd8ec 100644
--- a/src/state/reducers/workspace.js
+++ b/src/state/reducers/workspace.js
@@ -9,6 +9,8 @@ const workspaceReducer = (state = {}, action) => {
       return { ...state, focusedWindowId: action.windowId };
     case ActionTypes.FULLSCREEN_WORKSPACE:
       return { ...state, fullscreen: action.fullscreen };
+    case ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT:
+      return { ...state, layout: action.layout };
     default:
       return state;
   }
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 1c827c81a26a32a9e4259cadb2d3108f9c567f6d..53bd8bd43d86efe90949417827222e39568d5758 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -27,6 +27,7 @@ body {
   &-window {
     display: flex;
     flex-direction: column;
+    height: 100%;
   }
 
   &-osd-container {