diff --git a/__tests__/fixtures/version-2/collection.json b/__tests__/fixtures/version-2/collection.json
new file mode 100644
index 0000000000000000000000000000000000000000..3ad3559df4581cc7d2cf1a3e9fdcff5651339825
--- /dev/null
+++ b/__tests__/fixtures/version-2/collection.json
@@ -0,0 +1,292 @@
+{
+  "@context": "http://iiif.io/api/presentation/2/context.json", 
+  "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/collection.json", 
+  "@type": "sc:Collection", 
+  "label": "Collection of Test Cases", 
+  "manifests": [
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/1/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 1 Manifest: Minimum Required Fields"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/2/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 2 Manifest: Metadata Pairs"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/3/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 3 Manifest: Metadata Pairs with Languages"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/4/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 4 Manifest: Metadata Pairs with Multiple Values in same Language"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/5/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 5 Manifest: Description field"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/6/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 6 Manifest: Multiple Descriptions"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/7/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 7 Manifest: Rights Metadata"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/8/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 8 Manifest: SeeAlso link / Manifest"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/9/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 9 Manifest: Service link / Manifest"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/10/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 10 Manifest: Service link as Object"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/11/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 11 Manifest: ViewingDirection: l-t-r"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/12/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 12 Manifest: ViewingDirection: r-t-l"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/13/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 13 Manifest: ViewingDirection: t-t-b"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/14/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 14 Manifest: ViewingDirection: b-t-t"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/15/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 15 Manifest: ViewingHint: paged"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/16/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 16 Manifest: ViewingHint: continuous"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/17/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 17 Manifest: ViewingHint: individuals"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/18/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 18 Manifest: Non Standard Keys"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 19 Manifest: Multiple Canvases"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/20/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 20 Manifest: Multiple Sequences"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/21/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 21 Manifest: Sequence with Metadata"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/22/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 22 Manifest: /Sequence/ with non l-t-r viewingDirection"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/23/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 23 Manifest: /Sequence/ with non paged viewingHint"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/24/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 24 Manifest: Image with IIIF Service"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/25/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 25 Manifest: Image with IIIF Service, embedded info"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/26/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 26 Manifest: Image different size to Canvas"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/27/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 27 Manifest: No Image"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/28/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 28 Manifest: Choice of Image"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/29/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 29 Manifest: Choice of Image with IIIF Service"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/30/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 30 Manifest: Main + Detail Image"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/31/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 31 Manifest: Detail with IIIF Service"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/32/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 32 Manifest: Multiple Detail Images"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/33/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 33 Manifest: Detail Image with Choice"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/34/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 34 Manifest: Detail Image with Choice, and 'no image' as option"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/35/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 35 Manifest: Partial Image as Main Image"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/36/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 36 Manifest: Partial Image as Main Image with IIIF Service"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/37/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 37 Manifest: Partial Image as Detail Image"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/38/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 38 Manifest: Partial Image as Detail Image with IIIF Service"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/39/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 39 Manifest: Image with CSS Rotation"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/40/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 40 Manifest: Multiple Languages for Metadata Labels"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/41/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 41 Manifest: Main Image with Server side Rotation"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/43/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 43 Manifest: Embedded Transcription on Canvas"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/44/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 44 Manifest: Embedded Transcription on Fragment Segment"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/45/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 45 Manifest: External text/plain Transcription on Canvas"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/46/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 46 Manifest: External text/plain Transcription on Segment"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/47/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 47 Manifest: Embedded HTML Transcription on Canvas"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/48/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 48 Manifest: Embedded HTML Transcription on Segment"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/51/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 51 Manifest: Embedded Comment on a Canvas"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/52/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 52 Manifest: Embedded Comment on a Segment"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/54/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 54 Manifest: Comment in HTML"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/61/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 61 Manifest: Embedded Transcription on Selector Segment"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/62/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": [
+        {
+          "@value": "62: quelque titre", 
+          "@language": "fr"
+        }, 
+        {
+          "@value": "62: some title", 
+          "@language": "en"
+        }
+      ]
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/63/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 63 Manifest: Description in Multiple Languages"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/64/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 64 Manifest: Description in HTML"
+    }, 
+    {
+      "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/65/manifest.json", 
+      "@type": "sc:Manifest", 
+      "label": "Test 65 Manifest: Sequence with startCanvas"
+    }
+  ]
+}
diff --git a/__tests__/integration/mirador/collections.html b/__tests__/integration/mirador/collections.html
new file mode 100644
index 0000000000000000000000000000000000000000..8c29d6cb5e02a45792f9aa7f816e494a55948f79
--- /dev/null
+++ b/__tests__/integration/mirador/collections.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="theme-color" content="#000000">
+    <title>Mirador</title>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
+  </head>
+  <body>
+    <div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div>
+    <script>document.write("<script type='text/javascript' src='../../../dist/mirador.min.js?v=" + Date.now() + "'><\/script>");</script>
+    <script type="text/javascript">
+     var miradorInstance = Mirador.viewer({
+       id: 'mirador',
+       windows: [{
+         collectionPath: [
+            "https://www.e-codices.unifr.ch/metadata/iiif/collection.json",
+            "https://www.e-codices.unifr.ch/metadata/iiif/collection/stabs.json"
+          ],
+          manifestId: "https://www.e-codices.unifr.ch/metadata/iiif/stabs-StAlban-DD1-1580/manifest.json"
+       }],
+       catalog: [
+         { manifestId: "https://www.e-codices.unifr.ch/metadata/iiif/collection.json" },
+       ]
+     });
+    </script>
+  </body>
+</html>
diff --git a/__tests__/src/components/CollectionDialog.test.js b/__tests__/src/components/CollectionDialog.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..64928539f0aa225405a7bae2e02aadba52dbc90d
--- /dev/null
+++ b/__tests__/src/components/CollectionDialog.test.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import Button from '@material-ui/core/Button';
+import MenuItem from '@material-ui/core/MenuItem';
+import Skeleton from '@material-ui/lab/Skeleton';
+import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { CollectionDialog } from '../../../src/components/CollectionDialog';
+import collection from '../../fixtures/version-2/collection.json';
+
+/** */
+function createWrapper(props) {
+  const manifest = Utils.parseManifest(props.manifest ? props.manifest : collection);
+  return shallow(
+    <CollectionDialog
+      addWindow={() => {}}
+      classes={{}}
+      ready
+      manifest={manifest}
+      t={(key) => key}
+      {...props}
+    />,
+  );
+}
+
+describe('CollectionDialog', () => {
+  it('renders a dialog with collection menu items', () => {
+    const wrapper = createWrapper({});
+    expect(wrapper.find(Dialog).length).toEqual(1);
+    expect(wrapper.find(MenuItem).length).toEqual(55);
+    expect(wrapper.find(MenuItem).first().text()).toEqual('Test 1 Manifest: Minimum Required Fields');
+  });
+  it('when not ready returns placeholder skeleton', () => {
+    const wrapper = createWrapper({ ready: false });
+    expect(wrapper.find(Skeleton).length).toEqual(3);
+  });
+  it('clicking the hide button fires hideCollectionDialog', () => {
+    const hideCollectionDialog = jest.fn();
+    const wrapper = createWrapper({ hideCollectionDialog });
+    expect(wrapper.find(DialogActions).find(Button).first().simulate('click'));
+    expect(hideCollectionDialog).toHaveBeenCalled();
+  });
+});
diff --git a/__tests__/src/components/CollectionInfo.test.js b/__tests__/src/components/CollectionInfo.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..b990a49f7213b10ba4f5f65d302081c81e2d9329
--- /dev/null
+++ b/__tests__/src/components/CollectionInfo.test.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Button from '@material-ui/core/Button';
+import { CollectionInfo } from '../../../src/components/CollectionInfo';
+import CollapsibleSection from '../../../src/containers/CollapsibleSection';
+
+/** */
+function createWrapper(props) {
+  return shallow(
+    <CollectionInfo
+      id="test"
+      collectionPath={[1, 2]}
+      showCollectionDialog={() => {}}
+      {...props}
+    />,
+  );
+}
+
+describe('CollectionInfo', () => {
+  it('renders a collapsible section', () => {
+    const wrapper = createWrapper();
+    expect(wrapper.find(CollapsibleSection).length).toEqual(1);
+  });
+  it('without a collectionPath, renders nothing', () => {
+    const wrapper = createWrapper({ collectionPath: [] });
+    expect(wrapper.find(CollapsibleSection).length).toEqual(0);
+  });
+  it('clicking the button fires showCollectionDialog', () => {
+    const showCollectionDialog = jest.fn();
+    const wrapper = createWrapper({ showCollectionDialog });
+    expect(wrapper.find(Button).first().simulate('click'));
+    expect(showCollectionDialog).toHaveBeenCalled();
+  });
+});
diff --git a/__tests__/src/components/ManifestListItem.test.js b/__tests__/src/components/ManifestListItem.test.js
index 9f6d27f36b5a9f4f12609d2c1573e27ec28f75c5..72f183c722c5475f662bbb502e64e50934080eca 100644
--- a/__tests__/src/components/ManifestListItem.test.js
+++ b/__tests__/src/components/ManifestListItem.test.js
@@ -67,4 +67,16 @@ describe('ManifestListItem', () => {
     const wrapper = createWrapper();
     expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('addedFromUrl');
   });
+
+  it('when clicking a collection fires the showCollectionDialog', () => {
+    const showCollectionDialog = jest.fn();
+    const wrapper = createWrapper({ isCollection: true, showCollectionDialog });
+    wrapper.find(ButtonBase).simulate('click');
+    expect(showCollectionDialog).toHaveBeenCalledTimes(1);
+  });
+
+  it('displays a collection label for collections', () => {
+    const wrapper = createWrapper({ isCollection: true });
+    expect(wrapper.text()).toContain('collectionxyz');
+  });
 });
diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js
index 5d379cd7972caee138e3385e7c2051fa480d8090..04107ca69dd4c5ef8910b4704128d6ba46e40bce 100644
--- a/__tests__/src/lib/MiradorViewer.test.js
+++ b/__tests__/src/lib/MiradorViewer.test.js
@@ -58,10 +58,10 @@ describe('MiradorViewer', () => {
       expect(windows[windowIds[0]].view).toBe(undefined);
       expect(windows[windowIds[1]].view).toBe('book');
 
-      expect(catalog.length).toBe(1);
-      expect(catalog[0].manifestId).toBe('http://media.nga.gov/public/manifests/nga_highlights.json');
-      expect(catalog[0].provider).toBe('National Gallery of Art');
-
+      expect(catalog.length).toBe(2);
+      expect(catalog[0].manifestId).toBe('https://iiif.harvardartmuseums.org/manifests/object/299843');
+      expect(catalog[1].manifestId).toBe('http://media.nga.gov/public/manifests/nga_highlights.json');
+      expect(catalog[1].provider).toBe('National Gallery of Art');
       expect(config.foo).toBe('bar');
     });
     it('merges translation configs from multiple plugins', () => {
diff --git a/__tests__/src/reducers/catalog.test.js b/__tests__/src/reducers/catalog.test.js
index edd4db6dd6e5b7facbd39e0007e24a38b234fb95..b232590e94f29e8ef231f7c688ad63a6965908e2 100644
--- a/__tests__/src/reducers/catalog.test.js
+++ b/__tests__/src/reducers/catalog.test.js
@@ -39,19 +39,19 @@ describe('catalog reducer', () => {
     });
   });
 
-  describe('REQUEST_MANIFEST', () => {
+  describe('ADD_WINDOW', () => {
     it('adds new manifests to the state', () => {
       expect(catalogReducer([], {
-        manifestId: '1',
-        type: ActionTypes.REQUEST_MANIFEST,
+        type: ActionTypes.ADD_WINDOW,
+        window: { manifestId: '1' },
       })).toEqual([
         { manifestId: '1' },
       ]);
     });
     it('adds new manifests to the top of state', () => {
       expect(catalogReducer([{ manifestId: '2' }], {
-        manifestId: '1',
-        type: ActionTypes.REQUEST_MANIFEST,
+        type: ActionTypes.ADD_WINDOW,
+        window: { manifestId: '1' },
       })).toEqual([
         { manifestId: '1' },
         { manifestId: '2' },
@@ -59,8 +59,8 @@ describe('catalog reducer', () => {
     });
     it('deduplicate manifests', () => {
       expect(catalogReducer([{ manifestId: '1' }], {
-        manifestId: '1',
-        type: ActionTypes.REQUEST_MANIFEST,
+        type: ActionTypes.ADD_WINDOW,
+        window: { manifestId: '1' },
       })).toEqual([
         { manifestId: '1' },
       ]);
diff --git a/__tests__/src/sagas/app.test.js b/__tests__/src/sagas/app.test.js
index ce4467d0939cee103190f468ddb0deaba5f3e2d5..193f21cdb694cc3ed529f8e45c2cdec03a994352 100644
--- a/__tests__/src/sagas/app.test.js
+++ b/__tests__/src/sagas/app.test.js
@@ -2,7 +2,7 @@ import { call } from 'redux-saga/effects';
 import { expectSaga, testSaga } from 'redux-saga-test-plan';
 
 import { importConfig, importState } from '../../../src/state/sagas/app';
-import { fetchManifest } from '../../../src/state/sagas/iiif';
+import { fetchManifests } from '../../../src/state/sagas/iiif';
 import { fetchWindowManifest } from '../../../src/state/sagas/windows';
 import { addWindow } from '../../../src/state/actions';
 
@@ -26,7 +26,7 @@ describe('app-level sagas', () => {
           call(fetchWindowManifest, { id: 'y', payload: { id: 'y', manifestId: 'url2' } }),
         ]);
     });
-    it('calls into fetchManifest for each manifest', () => {
+    it('calls into fetchManifests for each manifest', () => {
       const action = {
         state: {
           manifests: { x: { id: 'x' } },
@@ -37,7 +37,7 @@ describe('app-level sagas', () => {
       testSaga(importState, action)
         .next()
         .all([
-          call(fetchManifest, { manifestId: 'x' }),
+          call(fetchManifests, 'x'),
         ]);
     });
     it('does not fetchManifest if the manifest json was provided', () => {
diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js
index f8a696388fe75184a133279333f48a23ace68934..e900713f12b804e17e8455d33c2b08b41c953f92 100644
--- a/__tests__/src/sagas/windows.test.js
+++ b/__tests__/src/sagas/windows.test.js
@@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan';
 import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 
 import ActionTypes from '../../../src/state/actions/action-types';
-import { receiveManifest, setCanvas } from '../../../src/state/actions';
+import { setCanvas } from '../../../src/state/actions';
 import {
   getManifests, getManifestoInstance,
   getManifestSearchService, getCompanionWindowIdsForPosition,
@@ -15,7 +15,7 @@ import {
   getVisibleCanvasIds, getCanvasForAnnotation,
   getCanvases, selectInfoResponses,
 } from '../../../src/state/selectors';
-import { fetchManifest } from '../../../src/state/sagas/iiif';
+import { fetchManifests } from '../../../src/state/sagas/iiif';
 import {
   fetchWindowManifest,
   setWindowDefaultSearchQuery,
@@ -27,12 +27,13 @@ import {
   panToFocusedWindow,
   setCurrentAnnotationsOnCurrentCanvas,
   fetchInfoResponses,
+  setCollectionPath,
 } from '../../../src/state/sagas/windows';
 import fixture from '../../fixtures/version-2/019.json';
 
 describe('window-level sagas', () => {
   describe('fetchWindowManifest', () => {
-    it('calls into fetchManifest for each window', () => {
+    it('calls into fetchManifests for each window', () => {
       const action = {
         window: {
           id: 'x',
@@ -43,48 +44,12 @@ describe('window-level sagas', () => {
       return expectSaga(fetchWindowManifest, action)
         .provide([
           [select(getManifests), {}],
-          [call(fetchManifest, { manifestId: 'manifest.json' }), {}],
+          [call(fetchManifests, 'manifest.json'), {}],
           [call(setWindowStartingCanvas, action)],
           [call(setWindowDefaultSearchQuery, action)],
+          [call(setCollectionPath, { manifestId: 'manifest.json', windowId: 'x' })],
         ])
-        .call(fetchManifest, { manifestId: 'manifest.json' })
-        .run();
-    });
-
-    it('calls retrieveManifest if a manifest was provided', () => {
-      const manifestJson = { data: '123' };
-      const action = {
-        manifest: manifestJson,
-        window: {
-          id: 'x',
-          manifestId: 'manifest.json',
-        },
-      };
-
-      return expectSaga(fetchWindowManifest, action)
-        .provide([
-          [call(setWindowStartingCanvas, action)],
-          [call(setWindowDefaultSearchQuery, action)],
-        ])
-        .put(receiveManifest('manifest.json', manifestJson))
-        .run();
-    });
-
-    it('does not call fetchManifest if the manifest is already available', () => {
-      const action = {
-        window: {
-          id: 'x',
-          manifestId: 'manifest.json',
-        },
-      };
-
-      return expectSaga(fetchWindowManifest, action)
-        .provide([
-          [select(getManifests), { 'manifest.json': {} }],
-          [call(setWindowStartingCanvas, action)],
-          [call(setWindowDefaultSearchQuery, action)],
-        ])
-        .not.call(fetchManifest, { manifestId: 'manifest.json' })
+        .call(fetchManifests, 'manifest.json')
         .run();
     });
     it('calls additional methods after ensuring we have a manifest', () => {
@@ -100,6 +65,7 @@ describe('window-level sagas', () => {
           [select(getManifests), { 'manifest.json': {} }],
           [call(setWindowStartingCanvas, action)],
           [call(setWindowDefaultSearchQuery, action)],
+          [call(setCollectionPath, { manifestId: 'manifest.json', windowId: 'x' })],
         ])
         .call(setWindowStartingCanvas, action)
         .call(setWindowDefaultSearchQuery, action)
@@ -117,7 +83,7 @@ describe('window-level sagas', () => {
         },
       };
 
-      return expectSaga(fetchWindowManifest, action)
+      return expectSaga(setWindowStartingCanvas, action)
         .provide([
           [select(getManifests), { 'manifest.json': {} }],
           [call(setCanvas, 'x', '1', null, { preserveViewport: false }), { type: 'setCanvasThunk' }],
@@ -135,7 +101,7 @@ describe('window-level sagas', () => {
         },
       };
 
-      return expectSaga(fetchWindowManifest, action)
+      return expectSaga(setWindowStartingCanvas, action)
         .provide([
           [select(getManifests), { 'manifest.json': {} }],
           [call(setCanvas, 'x', '1', null, { preserveViewport: true }), { type: 'setCanvasThunk' }],
diff --git a/src/components/CollectionDialog.js b/src/components/CollectionDialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..59730884c4e6500cf80b40a554067096a217c909
--- /dev/null
+++ b/src/components/CollectionDialog.js
@@ -0,0 +1,267 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import {
+  Button,
+  Chip,
+  Dialog,
+  DialogActions,
+  DialogTitle,
+  Link,
+  MenuList,
+  MenuItem,
+  Typography,
+} from '@material-ui/core';
+import ArrowBackIcon from '@material-ui/icons/ArrowBackSharp';
+import Skeleton from '@material-ui/lab/Skeleton';
+import { LabelValueMetadata } from './LabelValueMetadata';
+import CollapsibleSection from '../containers/CollapsibleSection';
+import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent';
+import ManifestInfo from '../containers/ManifestInfo';
+
+/**
+ */
+function asArray(value) {
+  if (!Array.isArray(value)) {
+    return [value];
+  }
+
+  return value;
+}
+
+/**
+ * a simple dialog providing the possibility to switch the theme
+ */
+export class CollectionDialog extends Component {
+  /** */
+  static getUseableLabel(resource, index) {
+    return (resource
+      && resource.getLabel
+      && resource.getLabel().length > 0)
+      ? resource.getLabel().map(label => label.value)[0]
+      : String(index + 1);
+  }
+
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.state = { filter: null };
+  }
+
+  /** */
+  setFilter(filter) {
+    this.setState({ filter });
+  }
+
+  /** */
+  selectCollection(c) {
+    const {
+      collectionPath,
+      manifestId,
+      showCollectionDialog,
+      windowId,
+    } = this.props;
+
+    showCollectionDialog(c.id, [...collectionPath, manifestId], windowId);
+  }
+
+  /** */
+  goToPreviousCollection() {
+    const { collectionPath, showCollectionDialog, windowId } = this.props;
+
+    showCollectionDialog(
+      collectionPath[collectionPath.length - 1],
+      collectionPath.slice(0, -1),
+      windowId,
+    );
+  }
+
+  /** */
+  selectManifest(m) {
+    const {
+      addWindow,
+      collectionPath,
+      hideCollectionDialog,
+      manifestId,
+      setWorkspaceAddVisibility,
+      updateWindow,
+      windowId,
+    } = this.props;
+
+    if (windowId) {
+      updateWindow(windowId, {
+        canvasId: null, collectionPath: [...collectionPath, manifestId], manifestId: m.id,
+      });
+    } else {
+      addWindow({ collectionPath: [...collectionPath, manifestId], manifestId: m.id });
+    }
+
+    hideCollectionDialog();
+    setWorkspaceAddVisibility(false);
+  }
+
+  /** */
+  placeholder() {
+    const { classes, hideCollectionDialog } = this.props;
+
+    return (
+      <Dialog
+        onClose={hideCollectionDialog}
+        open
+      >
+        <DialogTitle id="select-collection" disableTypography>
+          <Skeleton className={classes.placeholder} variant="text" />
+        </DialogTitle>
+        <ScrollIndicatedDialogContent>
+          <Skeleton className={classes.placeholder} variant="text" />
+          <Skeleton className={classes.placeholder} variant="text" />
+        </ScrollIndicatedDialogContent>
+      </Dialog>
+    );
+  }
+
+  /** */
+  render() {
+    const {
+      classes,
+      collection,
+      error,
+      hideCollectionDialog,
+      isMultipart,
+      manifest,
+      ready,
+      t,
+    } = this.props;
+
+    const { filter } = this.state;
+
+    if (error) return null;
+    if (!ready) return this.placeholder();
+
+    const rights = manifest && (asArray(manifest.getProperty('rights') || manifest.getProperty('license')));
+
+    const requiredStatement = manifest
+      && asArray(manifest.getRequiredStatement()).filter(l => l.getValue()).map(labelValuePair => ({
+        label: labelValuePair.getLabel(),
+        values: labelValuePair.getValues(),
+      }));
+
+    const collections = manifest.getCollections();
+
+    const currentFilter = filter || (collections.length > 0 ? 'collections' : 'manifests');
+
+    return (
+      <Dialog
+        onClose={hideCollectionDialog}
+        open
+      >
+        <DialogTitle id="select-collection" disableTypography>
+          <Typography component="div" variant="overline">
+            { t(isMultipart ? 'multipartCollection' : 'collection') }
+          </Typography>
+          <Typography variant="h3">
+            {CollectionDialog.getUseableLabel(manifest)}
+          </Typography>
+        </DialogTitle>
+        <ScrollIndicatedDialogContent className={classes.dialogContent}>
+          { collection && (
+            <Button
+              startIcon={<ArrowBackIcon />}
+              onClick={() => this.goToPreviousCollection()}
+            >
+              {CollectionDialog.getUseableLabel(collection)}
+            </Button>
+          )}
+
+          <div className={classes.collectionMetadata}>
+            <ManifestInfo manifestId={manifest.id} />
+            <CollapsibleSection
+              id="select-collection-rights"
+              label={t('attributionTitle')}
+            >
+              { requiredStatement && (
+                <LabelValueMetadata labelValuePairs={requiredStatement} defaultLabel={t('attribution')} />
+              )}
+              {
+                rights && rights.length > 0 && (
+                  <>
+                    <Typography variant="subtitle2" component="dt">{t('rights')}</Typography>
+                    { rights.map(v => (
+                      <Typography variant="body1" component="dd">
+                        <Link target="_blank" rel="noopener noreferrer" href={v}>
+                          {v}
+                        </Link>
+                      </Typography>
+                    )) }
+                  </>
+                )
+              }
+            </CollapsibleSection>
+          </div>
+          <div className={classes.collectionFilter}>
+            {manifest.getTotalCollections() > 0 && (
+              <Chip clickable color={currentFilter === 'collections' ? 'primary' : 'default'} onClick={() => this.setFilter('collections')} label={t('totalCollections', { count: manifest.getTotalCollections() })} />
+            )}
+            {manifest.getTotalManifests() > 0 && (
+              <Chip clickable color={currentFilter === 'manifests' ? 'primary' : 'default'} onClick={() => this.setFilter('manifests')} label={t('totalManifests', { count: manifest.getTotalManifests() })} />
+            )}
+          </div>
+          { currentFilter === 'collections' && (
+            <MenuList>
+              {
+                collections.map(c => (
+                  <MenuItem key={c.id} onClick={() => { this.selectCollection(c); }}>
+                    {CollectionDialog.getUseableLabel(c)}
+                  </MenuItem>
+                ))
+              }
+            </MenuList>
+          )}
+          { currentFilter === 'manifests' && (
+            <MenuList>
+              {
+                manifest.getManifests().map(m => (
+                  <MenuItem key={m.id} onClick={() => { this.selectManifest(m); }}>
+                    {CollectionDialog.getUseableLabel(m)}
+                  </MenuItem>
+                ))
+              }
+            </MenuList>
+          )}
+        </ScrollIndicatedDialogContent>
+        <DialogActions>
+          <Button onClick={hideCollectionDialog}>
+            {t('close')}
+          </Button>
+        </DialogActions>
+      </Dialog>
+    );
+  }
+}
+
+CollectionDialog.propTypes = {
+  addWindow: PropTypes.func.isRequired,
+  classes: PropTypes.objectOf(PropTypes.string).isRequired,
+  collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+  collectionPath: PropTypes.arrayOf(PropTypes.string),
+  error: PropTypes.string,
+  hideCollectionDialog: PropTypes.func.isRequired,
+  isMultipart: PropTypes.bool,
+  manifest: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+  manifestId: PropTypes.string.isRequired,
+  ready: PropTypes.bool,
+  setWorkspaceAddVisibility: PropTypes.func.isRequired,
+  showCollectionDialog: PropTypes.func.isRequired,
+  t: PropTypes.func.isRequired,
+  updateWindow: PropTypes.func.isRequired,
+  windowId: PropTypes.string,
+};
+
+CollectionDialog.defaultProps = {
+  collection: null,
+  collectionPath: [],
+  error: null,
+  isMultipart: false,
+  ready: false,
+  windowId: null,
+};
diff --git a/src/components/CollectionInfo.js b/src/components/CollectionInfo.js
new file mode 100644
index 0000000000000000000000000000000000000000..15670b1c98db518dddeac26431b457bf1e1596a5
--- /dev/null
+++ b/src/components/CollectionInfo.js
@@ -0,0 +1,83 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Button from '@material-ui/core/Button';
+import Typography from '@material-ui/core/Typography';
+import ViewListIcon from '@material-ui/icons/ViewListSharp';
+import CollapsibleSection from '../containers/CollapsibleSection';
+
+/**
+ * ManifestInfo
+ */
+export class CollectionInfo extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.openCollectionDialog = this.openCollectionDialog.bind(this);
+  }
+
+  /** */
+  openCollectionDialog() {
+    const { collectionPath, showCollectionDialog, windowId } = this.props;
+
+    const manifestId = collectionPath[collectionPath.length - 1];
+
+    showCollectionDialog(manifestId, collectionPath.slice(0, -1), windowId);
+  }
+
+  /**
+   * render
+   * @return
+   */
+  render() {
+    const {
+      collectionLabel,
+      collectionPath,
+      id,
+      t,
+    } = this.props;
+
+    if (collectionPath.length === 0) return null;
+
+    return (
+      <CollapsibleSection
+        id={`${id}-collection`}
+        label={t('collection')}
+      >
+        {collectionLabel && (
+          <Typography
+            aria-labelledby={`${id}-resource ${id}-resource-heading`}
+            id={`${id}-resource-heading`}
+            variant="h4"
+          >
+            {collectionLabel}
+          </Typography>
+        )}
+
+        <Button
+          color="primary"
+          onClick={this.openCollectionDialog}
+          startIcon={<ViewListIcon />}
+        >
+          {t('showCollection')}
+        </Button>
+      </CollapsibleSection>
+    );
+  }
+}
+
+CollectionInfo.propTypes = {
+  collectionLabel: PropTypes.string,
+  collectionPath: PropTypes.arrayOf(PropTypes.string),
+  id: PropTypes.string.isRequired,
+  showCollectionDialog: PropTypes.func.isRequired,
+  t: PropTypes.func,
+  windowId: PropTypes.string,
+};
+
+CollectionInfo.defaultProps = {
+  collectionLabel: null,
+  collectionPath: [],
+  t: key => key,
+  windowId: null,
+};
diff --git a/src/components/ManifestListItem.js b/src/components/ManifestListItem.js
index bf70127676f4640847963f53dab8521d98f41402..8333ca28714ae51568bf20f6b3b1742a9ba959e3 100644
--- a/src/components/ManifestListItem.js
+++ b/src/components/ManifestListItem.js
@@ -9,12 +9,6 @@ import { Img } from 'react-image';
 import ManifestListItemError from '../containers/ManifestListItemError';
 import ns from '../config/css-ns';
 
-/**
- * Handling open button click
- */
-const handleOpenButtonClick = (event, manifest, addWindow) => {
-  addWindow({ manifestId: manifest });
-};
 /**
  * Represents an item in a list of currently-loaded or loading manifests
  * @param {object} props
@@ -23,6 +17,12 @@ const handleOpenButtonClick = (event, manifest, addWindow) => {
 
 /** */
 export class ManifestListItem extends React.Component {
+  /** */
+  constructor(props) {
+    super(props);
+    this.handleOpenButtonClick = this.handleOpenButtonClick.bind(this);
+  }
+
   /** */
   componentDidMount() {
     const {
@@ -32,6 +32,26 @@ export class ManifestListItem extends React.Component {
     if (!ready && !error && !isFetching && provider !== 'file') fetchManifest(manifestId);
   }
 
+  /**
+   * Handling open button click
+   */
+  handleOpenButtonClick() {
+    const {
+      addWindow,
+      handleClose,
+      manifestId,
+      showCollectionDialog,
+      isCollection,
+    } = this.props;
+
+    if (isCollection) {
+      showCollectionDialog(manifestId);
+    } else {
+      addWindow({ manifestId });
+      handleClose();
+    }
+  }
+
   /** */
   render() {
     const {
@@ -42,13 +62,13 @@ export class ManifestListItem extends React.Component {
       title,
       thumbnail,
       manifestLogo,
-      addWindow,
-      handleClose,
       size,
       classes,
       provider,
       t,
       error,
+      isCollection,
+      isMultipart,
     } = this.props;
 
     const placeholder = (
@@ -86,9 +106,7 @@ export class ManifestListItem extends React.Component {
                 ref={buttonRef}
                 className={ns('manifest-list-item-title')}
                 style={{ width: '100%' }}
-                onClick={
-                  (event) => { handleOpenButtonClick(event, manifestId, addWindow); handleClose(); }
-                }
+                onClick={this.handleOpenButtonClick}
               >
                 <Grid container spacing={2} className={classes.label} component="span">
                   <Grid item xs={4} sm={3} component="span">
@@ -109,6 +127,11 @@ export class ManifestListItem extends React.Component {
                     />
                   </Grid>
                   <Grid item xs={8} sm={9} component="span">
+                    { isCollection && (
+                      <Typography component="div" variant="overline">
+                        { t(isMultipart ? 'multipartCollection' : 'collection') }
+                      </Typography>
+                    )}
                     <Typography component="span" variant="h6">
                       {title || manifestId}
                     </Typography>
@@ -155,11 +178,14 @@ ManifestListItem.propTypes = {
   error: PropTypes.string,
   fetchManifest: PropTypes.func.isRequired,
   handleClose: PropTypes.func,
+  isCollection: PropTypes.bool,
   isFetching: PropTypes.bool,
+  isMultipart: PropTypes.bool,
   manifestId: PropTypes.string.isRequired,
   manifestLogo: PropTypes.string,
   provider: PropTypes.string,
   ready: PropTypes.bool,
+  showCollectionDialog: PropTypes.func.isRequired,
   size: PropTypes.number,
   t: PropTypes.func,
   thumbnail: PropTypes.string,
@@ -172,7 +198,9 @@ ManifestListItem.defaultProps = {
   classes: {},
   error: null,
   handleClose: () => {},
+  isCollection: false,
   isFetching: false,
+  isMultipart: false,
   manifestLogo: null,
   provider: null,
   ready: false,
diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js
index 76ca76d8f918651b083d2262bc68e1dfa93ac93d..0100d965d0329b39620dc8dc2e16901cfe1eb717 100644
--- a/src/components/WindowSideBarCanvasPanel.js
+++ b/src/components/WindowSideBarCanvasPanel.js
@@ -3,10 +3,13 @@ import PropTypes from 'prop-types';
 import Tabs from '@material-ui/core/Tabs';
 import Tab from '@material-ui/core/Tab';
 import Tooltip from '@material-ui/core/Tooltip';
+import Button from '@material-ui/core/Button';
 import RootRef from '@material-ui/core/RootRef';
 import ItemListIcon from '@material-ui/icons/ReorderSharp';
 import TocIcon from '@material-ui/icons/SortSharp';
 import ThumbnailListIcon from '@material-ui/icons/ViewListSharp';
+import Typography from '@material-ui/core/Typography';
+import ArrowForwardIcon from '@material-ui/icons/ArrowForwardSharp';
 import CompanionWindow from '../containers/CompanionWindow';
 import SidebarIndexList from '../containers/SidebarIndexList';
 import SidebarIndexTableOfContents from '../containers/SidebarIndexTableOfContents';
@@ -23,6 +26,15 @@ export class WindowSideBarCanvasPanel extends Component {
     this.containerRef = React.createRef();
   }
 
+  /** */
+  static getUseableLabel(resource, index) {
+    return (resource
+      && resource.getLabel
+      && resource.getLabel().length > 0)
+      ? resource.getLabel().map(label => label.value)[0]
+      : resource.id;
+  }
+
   /** @private */
   handleVariantChange(event, value) {
     const { updateVariant } = this.props;
@@ -36,7 +48,9 @@ export class WindowSideBarCanvasPanel extends Component {
   render() {
     const {
       classes,
+      collection,
       id,
+      showMultipart,
       t,
       variant,
       showToc,
@@ -44,6 +58,7 @@ export class WindowSideBarCanvasPanel extends Component {
     } = this.props;
 
     let listComponent;
+
     if (variant === 'tableOfContents') {
       listComponent = (
         <SidebarIndexTableOfContents
@@ -85,6 +100,17 @@ export class WindowSideBarCanvasPanel extends Component {
           )}
         >
           <div id={`tab-panel-${id}`}>
+            { collection && (
+              <Button
+                fullWidth
+                onClick={showMultipart}
+                endIcon={<ArrowForwardIcon />}
+              >
+                <Typography className={classes.collectionNavigationButton}>
+                  {WindowSideBarCanvasPanel.getUseableLabel(collection)}
+                </Typography>
+              </Button>
+            )}
             {listComponent}
           </div>
         </CompanionWindow>
@@ -95,7 +121,9 @@ export class WindowSideBarCanvasPanel extends Component {
 
 WindowSideBarCanvasPanel.propTypes = {
   classes: PropTypes.objectOf(PropTypes.string).isRequired,
+  collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types
   id: PropTypes.string.isRequired,
+  showMultipart: PropTypes.func.isRequired,
   showToc: PropTypes.bool,
   t: PropTypes.func.isRequired,
   updateVariant: PropTypes.func.isRequired,
@@ -104,5 +132,6 @@ WindowSideBarCanvasPanel.propTypes = {
 };
 
 WindowSideBarCanvasPanel.defaultProps = {
+  collection: null,
   showToc: false,
 };
diff --git a/src/components/WindowSideBarCollectionPanel.js b/src/components/WindowSideBarCollectionPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..baa41c951c26e1fea9571e3a0afe6e3b04b34ca6
--- /dev/null
+++ b/src/components/WindowSideBarCollectionPanel.js
@@ -0,0 +1,186 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import MenuList from '@material-ui/core/MenuList';
+import MenuItem from '@material-ui/core/MenuItem';
+import Typography from '@material-ui/core/Typography';
+import Skeleton from '@material-ui/lab/Skeleton';
+import ArrowUpwardIcon from '@material-ui/icons/ArrowUpwardSharp';
+import CompanionWindow from '../containers/CompanionWindow';
+import IIIFThumbnail from '../containers/IIIFThumbnail';
+
+/** */
+export class WindowSideBarCollectionPanel extends Component {
+  /** */
+  static getUseableLabel(resource, index) {
+    return (resource
+      && resource.getLabel
+      && resource.getLabel().length > 0)
+      ? resource.getLabel().map(label => label.value)[0]
+      : resource.id;
+  }
+
+  /** */
+  isMultipart() {
+    const { collection } = this.props;
+
+    if (!collection) return false;
+
+    const behaviors = collection.getProperty('behavior');
+
+    if (Array.isArray(behaviors)) return collection.includes('multi-part');
+
+    return behaviors === 'multi-part';
+  }
+
+  /** */
+  render() {
+    const {
+      canvasNavigation,
+      classes,
+      collectionPath,
+      collection,
+      id,
+      isFetching,
+      manifestId,
+      parentCollection,
+      updateCompanionWindow,
+      updateWindow,
+      t,
+      variant,
+      windowId,
+    } = this.props;
+
+    /** */
+    const Item = ({ manifest, ...otherProps }) => (
+      <MenuItem
+        className={classes.menuItem}
+        alignItems="flex-start"
+        button
+        component="li"
+        selected={manifestId === manifest.id}
+        {...otherProps}
+      >
+        { variant === 'thumbnail' && (
+          <ListItemIcon>
+            <IIIFThumbnail
+              resource={manifest}
+              maxHeight={canvasNavigation.height}
+              maxWidth={canvasNavigation.width}
+            />
+          </ListItemIcon>
+        )}
+        <ListItemText>{WindowSideBarCollectionPanel.getUseableLabel(manifest)}</ListItemText>
+      </MenuItem>
+    );
+
+    return (
+      <CompanionWindow
+        title={t(this.isMultipart() ? 'multipartCollection' : 'collection')}
+        windowId={windowId}
+        id={id}
+        titleControls={(
+          <>
+            { parentCollection && (
+              <List>
+                <ListItem
+                  button
+                  onClick={
+                    () => updateCompanionWindow({ collectionPath: collectionPath.slice(0, -1) })
+                  }
+                >
+                  <ListItemIcon>
+                    <ArrowUpwardIcon />
+                  </ListItemIcon>
+                  <ListItemText primaryTypographyProps={{ variant: 'body1' }}>
+                    {WindowSideBarCollectionPanel.getUseableLabel(parentCollection)}
+                  </ListItemText>
+                </ListItem>
+              </List>
+            )}
+            <Typography variant="h6">
+              { collection && WindowSideBarCollectionPanel.getUseableLabel(collection)}
+              { isFetching && <Skeleton className={classes.placeholder} variant="text" />}
+            </Typography>
+          </>
+        )}
+      >
+        <MenuList>
+          { isFetching && (
+            <MenuItem>
+              <ListItemText>
+                <Skeleton className={classes.placeholder} variant="text" />
+                <Skeleton className={classes.placeholder} variant="text" />
+                <Skeleton className={classes.placeholder} variant="text" />
+              </ListItemText>
+            </MenuItem>
+          )}
+          {
+            collection && collection.getCollections().map((manifest) => {
+              /** select the new manifest and go back to the normal index */
+              const onClick = () => {
+                // close collection
+                updateCompanionWindow({ collectionPath: [...collectionPath, manifest.id] });
+              };
+
+              return (
+                <Item key={manifest.id} onClick={onClick} manifest={manifest} />
+              );
+            })
+          }
+          {
+            collection && collection.getManifests().map((manifest) => {
+              /** select the new manifest and go back to the normal index */
+              const onClick = () => {
+                // select new manifest
+                updateWindow({ canvasId: null, collectionPath, manifestId: manifest.id });
+                // close collection
+                updateCompanionWindow({ multipart: false });
+              };
+
+              return (
+                <Item key={manifest.id} onClick={onClick} manifest={manifest} />
+              );
+            })
+          }
+        </MenuList>
+      </CompanionWindow>
+    );
+  }
+}
+
+WindowSideBarCollectionPanel.propTypes = {
+  canvasNavigation: PropTypes.shape({
+    height: PropTypes.number,
+    width: PropTypes.number,
+  }).isRequired,
+  classes: PropTypes.objectOf(PropTypes.string).isRequired,
+  collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+  collectionId: PropTypes.string.isRequired,
+  collectionPath: PropTypes.arrayOf(PropTypes.string),
+  error: PropTypes.string,
+  id: PropTypes.string.isRequired,
+  isFetching: PropTypes.bool,
+  manifestId: PropTypes.string.isRequired,
+  parentCollection: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+  ready: PropTypes.bool,
+  t: PropTypes.func,
+  updateCompanionWindow: PropTypes.func.isRequired,
+  updateWindow: PropTypes.func.isRequired,
+  variant: PropTypes.string,
+  windowId: PropTypes.string.isRequired,
+};
+
+WindowSideBarCollectionPanel.defaultProps = {
+  collection: null,
+  collectionPath: [],
+  error: null,
+  isFetching: false,
+  parentCollection: null,
+  ready: false,
+  t: k => k,
+  variant: null,
+};
diff --git a/src/components/WindowSideBarInfoPanel.js b/src/components/WindowSideBarInfoPanel.js
index f2530d26152e7fa47dc6a518d971a1f706bbabe3..176e0761c564c214b4f95cbd43a309a5d380490a 100644
--- a/src/components/WindowSideBarInfoPanel.js
+++ b/src/components/WindowSideBarInfoPanel.js
@@ -4,6 +4,7 @@ import CompanionWindow from '../containers/CompanionWindow';
 import CanvasInfo from '../containers/CanvasInfo';
 import LocalePicker from '../containers/LocalePicker';
 import ManifestInfo from '../containers/ManifestInfo';
+import CollectionInfo from '../containers/CollectionInfo';
 import ManifestRelatedLinks from '../containers/ManifestRelatedLinks';
 import ns from '../config/css-ns';
 
@@ -20,6 +21,7 @@ export class WindowSideBarInfoPanel extends Component {
       windowId,
       id,
       classes,
+      collectionPath,
       t,
       locale,
       selectedCanvases,
@@ -58,6 +60,11 @@ export class WindowSideBarInfoPanel extends Component {
             </div>
           ))
         }
+        { collectionPath.length > 0 && (
+          <div className={classes.section}>
+            <CollectionInfo id={id} windowId={windowId} />
+          </div>
+        )}
 
         <div className={classes.section}>
           <ManifestInfo id={id} windowId={windowId} />
@@ -74,6 +81,7 @@ export class WindowSideBarInfoPanel extends Component {
 WindowSideBarInfoPanel.propTypes = {
   availableLocales: PropTypes.arrayOf(PropTypes.string),
   classes: PropTypes.objectOf(PropTypes.string),
+  collectionPath: PropTypes.arrayOf(PropTypes.string),
   id: PropTypes.string.isRequired,
   locale: PropTypes.string,
   selectedCanvases: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string })),
@@ -86,6 +94,7 @@ WindowSideBarInfoPanel.propTypes = {
 WindowSideBarInfoPanel.defaultProps = {
   availableLocales: [],
   classes: {},
+  collectionPath: [],
   locale: '',
   selectedCanvases: [],
   setLocale: undefined,
diff --git a/src/components/WorkspaceArea.js b/src/components/WorkspaceArea.js
index 83439c3a7875e03239de7323a1cf987d5765694e..672c6a762dcaa64b4a10b96af4af5a1eabd9ff0e 100644
--- a/src/components/WorkspaceArea.js
+++ b/src/components/WorkspaceArea.js
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import CollectionDialog from '../containers/CollectionDialog';
 import ErrorDialog from '../containers/ErrorDialog';
 import WorkspaceControlPanel from '../containers/WorkspaceControlPanel';
 import Workspace from '../containers/Workspace';
@@ -19,8 +20,13 @@ export class WorkspaceArea extends Component {
    */
   render() {
     const {
-      classes, controlPanelVariant, isWorkspaceAddVisible, isWorkspaceControlPanelVisible, t,
+      classes,
+      controlPanelVariant,
+      isCollectionDialogVisible,
+      isWorkspaceAddVisible,
+      isWorkspaceControlPanelVisible,
       lang,
+      t,
     } = this.props;
 
     return (
@@ -41,6 +47,7 @@ export class WorkspaceArea extends Component {
           }
           <ErrorDialog />
           <BackgroundPluginArea />
+          { isCollectionDialogVisible && <CollectionDialog /> }
         </main>
       </>
     );
@@ -50,6 +57,7 @@ export class WorkspaceArea extends Component {
 WorkspaceArea.propTypes = {
   classes: PropTypes.objectOf(PropTypes.string).isRequired,
   controlPanelVariant: PropTypes.string,
+  isCollectionDialogVisible: PropTypes.bool,
   isWorkspaceAddVisible: PropTypes.bool,
   isWorkspaceControlPanelVisible: PropTypes.bool.isRequired,
   lang: PropTypes.string,
@@ -58,6 +66,7 @@ WorkspaceArea.propTypes = {
 
 WorkspaceArea.defaultProps = {
   controlPanelVariant: undefined,
+  isCollectionDialogVisible: false,
   isWorkspaceAddVisible: false,
   lang: undefined,
 };
diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js
index 749aef84bbeadd947c68dd32effc5e051ff2bcf9..f065ac60041750edc1ecb44f07aa47b5f0439006 100644
--- a/src/components/WorkspaceMosaic.js
+++ b/src/components/WorkspaceMosaic.js
@@ -5,7 +5,6 @@ import {
 } from 'react-mosaic-component';
 import 'react-mosaic-component/react-mosaic-component.css';
 import difference from 'lodash/difference';
-import toPairs from 'lodash/toPairs';
 import isEqual from 'lodash/isEqual';
 import classNames from 'classnames';
 import MosaicRenderPreview from '../containers/MosaicRenderPreview';
diff --git a/src/containers/CollectionDialog.js b/src/containers/CollectionDialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c1f687ed52e57b50e65a8223a5da446a47d3ebb
--- /dev/null
+++ b/src/containers/CollectionDialog.js
@@ -0,0 +1,84 @@
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { withStyles } from '@material-ui/core';
+import { withTranslation } from 'react-i18next';
+import { withPlugins } from '../extend/withPlugins';
+import * as actions from '../state/actions';
+import { getManifest, getManifestoInstance, getSequenceBehaviors } from '../state/selectors';
+import { CollectionDialog } from '../components/CollectionDialog';
+
+/**
+ * mapDispatchToProps - used to hook up connect to action creators
+ * @memberof CollectionDialog
+ * @private
+ */
+const mapDispatchToProps = {
+  addWindow: actions.addWindow,
+  hideCollectionDialog: actions.hideCollectionDialog,
+  setWorkspaceAddVisibility: actions.setWorkspaceAddVisibility,
+  showCollectionDialog: actions.showCollectionDialog,
+  updateWindow: actions.updateWindow,
+};
+
+/**
+ * mapStateToProps - to hook up connect
+ * @memberof CollectionDialog
+ * @private
+ */
+const mapStateToProps = (state) => {
+  const { collectionPath, collectionManifestId: manifestId } = state.workspace;
+  const manifest = getManifest(state, { manifestId });
+
+  const collectionId = collectionPath && collectionPath[collectionPath.length - 1];
+  const collection = collectionId && getManifest(state, { manifestId: collectionId });
+
+  return {
+    collection: collection && getManifestoInstance(state, { manifestId: collection.id }),
+    collectionPath,
+    error: manifest && manifest.error,
+    isMultipart: getSequenceBehaviors(state, { manifestId }).includes('multi-part'),
+    manifest: manifest && getManifestoInstance(state, { manifestId }),
+    manifestId,
+    open: state.workspace.collectionDialogOn,
+    ready: manifest && !!manifest.json,
+    windowId: state.workspace.collectionUpdateWindowId,
+  };
+};
+
+/** */
+const styles = theme => ({
+  collectionFilter: {
+    padding: '16px',
+    paddingTop: 0,
+  },
+  collectionMetadata: {
+    padding: '16px',
+  },
+  dark: {
+    color: '#000000',
+  },
+  dialogContent: {
+    padding: 0,
+  },
+  light: {
+    color: theme.palette.grey[400],
+  },
+  listitem: {
+    '&:focus': {
+      backgroundColor: theme.palette.action.focus,
+    },
+    '&:hover': {
+      backgroundColor: theme.palette.action.hover,
+    },
+    cursor: 'pointer',
+  },
+});
+
+const enhance = compose(
+  withTranslation(),
+  withStyles(styles),
+  connect(mapStateToProps, mapDispatchToProps),
+  withPlugins('CollectionDialog'),
+);
+
+export default enhance(CollectionDialog);
diff --git a/src/containers/CollectionInfo.js b/src/containers/CollectionInfo.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca686c0a5922f906c6d2ff8f66fa95891b09f424
--- /dev/null
+++ b/src/containers/CollectionInfo.js
@@ -0,0 +1,37 @@
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { withTranslation } from 'react-i18next';
+import { withPlugins } from '../extend/withPlugins';
+import {
+  getManifestTitle,
+  getWindow,
+} from '../state/selectors';
+import * as actions from '../state/actions';
+import { CollectionInfo } from '../components/CollectionInfo';
+
+/**
+ * mapStateToProps - to hook up connect
+ * @memberof WindowSideBarInfoPanel
+ * @private
+ */
+const mapStateToProps = (state, { id, windowId }) => {
+  const { collectionPath } = (getWindow(state, { windowId }) || {});
+  const manifestId = collectionPath[collectionPath.length - 1];
+
+  return {
+    collectionLabel: getManifestTitle(state, { manifestId }),
+    collectionPath,
+  };
+};
+
+const mapDispatchToProps = {
+  showCollectionDialog: actions.showCollectionDialog,
+};
+
+const enhance = compose(
+  withTranslation(),
+  connect(mapStateToProps, mapDispatchToProps),
+  withPlugins('CollectionInfo'),
+);
+
+export default enhance(CollectionInfo);
diff --git a/src/containers/ManifestInfo.js b/src/containers/ManifestInfo.js
index 5ae7a95a3f5f3a1df8729bc510730775db226bd1..737b87cf7ab78b17ca91e41e1d5b61e31a8876cb 100644
--- a/src/containers/ManifestInfo.js
+++ b/src/containers/ManifestInfo.js
@@ -14,10 +14,12 @@ import { ManifestInfo } from '../components/ManifestInfo';
  * @memberof WindowSideBarInfoPanel
  * @private
  */
-const mapStateToProps = (state, { id, windowId }) => ({
-  manifestDescription: getManifestDescription(state, { companionWindowId: id, windowId }),
-  manifestLabel: getManifestTitle(state, { companionWindowId: id, windowId }),
-  manifestMetadata: getManifestMetadata(state, { companionWindowId: id, windowId }),
+const mapStateToProps = (state, { id, manifestId, windowId }) => ({
+  manifestDescription: getManifestDescription(state, {
+    companionWindowId: id, manifestId, windowId,
+  }),
+  manifestLabel: getManifestTitle(state, { companionWindowId: id, manifestId, windowId }),
+  manifestMetadata: getManifestMetadata(state, { companionWindowId: id, manifestId, windowId }),
 });
 
 const enhance = compose(
diff --git a/src/containers/ManifestListItem.js b/src/containers/ManifestListItem.js
index 596c8a69e46097d7c2d1edb40d07c0fa1d08c098..20f2db58ae2132e4a32386d35d1b4a7dfc610318 100644
--- a/src/containers/ManifestListItem.js
+++ b/src/containers/ManifestListItem.js
@@ -7,6 +7,7 @@ import {
   getManifest,
   getManifestTitle, getManifestThumbnail, getCanvases,
   getManifestLogo, getManifestProvider, getWindowManifests,
+  getManifestoInstance, getSequenceBehaviors,
 } from '../state/selectors';
 import * as actions from '../state/actions';
 import { ManifestListItem } from '../components/ManifestListItem';
@@ -14,15 +15,26 @@ import { ManifestListItem } from '../components/ManifestListItem';
 /** */
 const mapStateToProps = (state, { manifestId, provider }) => {
   const manifest = getManifest(state, { manifestId }) || {};
+  const manifesto = getManifestoInstance(state, { manifestId });
+  const isCollection = (
+    manifesto || { isCollection: () => false }
+  ).isCollection();
+
+  const size = isCollection
+    ? manifesto.getTotalItems()
+    : getCanvases(state, { manifestId }).length;
   return {
     active: getWindowManifests(state).includes(manifestId),
     error: manifest.error,
+    isCollection,
     isFetching: manifest.isFetching,
+    isMultipart: isCollection
+      && getSequenceBehaviors(state, { manifestId }).includes('multi-part'),
     manifestLogo: getManifestLogo(state, { manifestId }),
     provider: provider
       || getManifestProvider(state, { manifestId }),
     ready: !!manifest.json,
-    size: getCanvases(state, { manifestId }).length,
+    size,
     thumbnail: getManifestThumbnail(state, { manifestId }),
     title: getManifestTitle(state, { manifestId }),
   };
@@ -33,7 +45,11 @@ const mapStateToProps = (state, { manifestId, provider }) => {
  * @memberof ManifestListItem
  * @private
  */
-const mapDispatchToProps = { addWindow: actions.addWindow, fetchManifest: actions.fetchManifest };
+const mapDispatchToProps = {
+  addWindow: actions.addWindow,
+  fetchManifest: actions.fetchManifest,
+  showCollectionDialog: actions.showCollectionDialog,
+};
 
 /**
  *
diff --git a/src/containers/WindowSideBarCanvasPanel.js b/src/containers/WindowSideBarCanvasPanel.js
index d2beb97166323a85c59bb36f3dcfa23cf51492da..a55744b1cb0b8b51008831ce1b70ede2629642e3 100644
--- a/src/containers/WindowSideBarCanvasPanel.js
+++ b/src/containers/WindowSideBarCanvasPanel.js
@@ -11,6 +11,8 @@ import {
   getCanvases,
   getVisibleCanvases,
   getSequenceTreeStructure,
+  getWindow,
+  getManifestoInstance,
 } from '../state/selectors';
 
 /**
@@ -19,13 +21,18 @@ import {
 const mapStateToProps = (state, { id, windowId }) => {
   const canvases = getCanvases(state, { windowId });
   const treeStructure = getSequenceTreeStructure(state, { windowId });
+  const window = getWindow(state, { windowId });
   const { config } = state;
+  const companionWindow = getCompanionWindow(state, { companionWindowId: id });
+  const collectionPath = window.collectionPath || [];
+  const collectionId = collectionPath && collectionPath[collectionPath.length - 1];
   return {
     canvases,
+    collection: collectionId && getManifestoInstance(state, { manifestId: collectionId }),
     config,
     selectedCanvases: getVisibleCanvases(state, { windowId }),
     showToc: treeStructure && treeStructure.nodes && treeStructure.nodes.length > 0,
-    variant: getCompanionWindow(state, { companionWindowId: id, windowId }).variant
+    variant: companionWindow.variant
       || getDefaultSidebarVariant(state, { windowId }),
   };
 };
@@ -37,6 +44,9 @@ const mapStateToProps = (state, { id, windowId }) => {
  */
 const mapDispatchToProps = (dispatch, { id, windowId }) => ({
   setCanvas: (...args) => dispatch(actions.setCanvas(...args)),
+  showMultipart: () => dispatch(
+    actions.addOrUpdateCompanionWindow(windowId, { content: 'collection', position: 'right' }),
+  ),
   toggleDraggingEnabled: () => dispatch(actions.toggleDraggingEnabled()),
   updateVariant: variant => dispatch(
     actions.updateCompanionWindow(windowId, id, { variant }),
@@ -48,6 +58,9 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({
  * @param theme
  */
 const styles = theme => ({
+  collectionNavigationButton: {
+    textTransform: 'none',
+  },
   label: {
     paddingLeft: theme.spacing(1),
   },
diff --git a/src/containers/WindowSideBarCollectionPanel.js b/src/containers/WindowSideBarCollectionPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..32b31a77b283f90756a568cffe44be7282f31d5b
--- /dev/null
+++ b/src/containers/WindowSideBarCollectionPanel.js
@@ -0,0 +1,80 @@
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { withTranslation } from 'react-i18next';
+import { withStyles } from '@material-ui/core/styles';
+import { withPlugins } from '../extend/withPlugins';
+import * as actions from '../state/actions';
+import {
+  getCompanionWindow,
+  getManifest,
+  getManifestoInstance,
+  getDefaultSidebarVariant,
+  getWindow,
+} from '../state/selectors';
+import { WindowSideBarCollectionPanel } from '../components/WindowSideBarCollectionPanel';
+
+/**
+ * mapStateToProps - to hook up connect
+ */
+const mapStateToProps = (state, { id, windowId }) => {
+  const window = getWindow(state, { windowId });
+  const companionWindow = getCompanionWindow(state, { companionWindowId: id });
+  const { collectionPath: localCollectionPath } = companionWindow;
+  const collectionPath = localCollectionPath || window.collectionPath;
+  const collectionId = collectionPath && collectionPath[collectionPath.length - 1];
+  const parentCollectionId = collectionPath && collectionPath[collectionPath.length - 2];
+  const collection = collectionId && getManifest(state, { manifestId: collectionId });
+  const parentCollection = parentCollectionId
+    && getManifest(state, { manifestId: parentCollectionId });
+  const manifest = getManifest(state, { windowId });
+
+  return {
+    canvasNavigation: state.config.canvasNavigation,
+    collection: collection && getManifestoInstance(state, { manifestId: collection.id }),
+    collectionId,
+    collectionPath,
+    error: collection && collection.error,
+    isFetching: collection && collection.isFetching,
+    manifestId: manifest && manifest.id,
+    parentCollection: parentCollection
+      && getManifestoInstance(state, { manifestId: parentCollection.id }),
+    ready: collection && !!collection.json,
+    variant: companionWindow.variant
+      || getDefaultSidebarVariant(state, { windowId }),
+  };
+};
+
+/**
+ * mapStateToProps - used to hook up connect to state
+ * @memberof SidebarIndexList
+ * @private
+ */
+const mapDispatchToProps = (dispatch, { id, windowId }) => ({
+  updateCompanionWindow: (...args) => dispatch(
+    actions.updateCompanionWindow(windowId, id, ...args),
+  ),
+  updateWindow: (...args) => dispatch(actions.updateWindow(windowId, ...args)),
+});
+
+/**
+ * Styles for withStyles HOC
+ */
+const styles = theme => ({
+  label: {
+    paddingLeft: theme.spacing(1),
+  },
+  menuItem: {
+    borderBottom: `0.5px solid ${theme.palette.divider}`,
+    paddingRight: theme.spacing(1),
+    whiteSpace: 'normal',
+  },
+});
+
+const enhance = compose(
+  withStyles(styles),
+  withTranslation(),
+  connect(mapStateToProps, mapDispatchToProps),
+  withPlugins('WindowSideBarCollectionPanel'),
+);
+
+export default enhance(WindowSideBarCollectionPanel);
diff --git a/src/containers/WindowSideBarInfoPanel.js b/src/containers/WindowSideBarInfoPanel.js
index 812dfad47ee483978bb62c1ef08746fda2f01250..0a69e62d0ebe70f5cc9079aea63e48c2725b1c94 100644
--- a/src/containers/WindowSideBarInfoPanel.js
+++ b/src/containers/WindowSideBarInfoPanel.js
@@ -10,6 +10,7 @@ import {
   getMetadataLocales,
   getVisibleCanvases,
   getWindowConfig,
+  getWindow,
 } from '../state/selectors';
 import { WindowSideBarInfoPanel } from '../components/WindowSideBarInfoPanel';
 
@@ -20,6 +21,7 @@ import { WindowSideBarInfoPanel } from '../components/WindowSideBarInfoPanel';
  */
 const mapStateToProps = (state, { id, windowId }) => ({
   availableLocales: getMetadataLocales(state, { companionWindowId: id, windowId }),
+  collectionPath: (getWindow(state, { windowId }) || {}).collectionPath,
   locale: getCompanionWindow(state, { companionWindowId: id }).locale
     || getManifestLocale(state, { windowId }),
   selectedCanvases: getVisibleCanvases(state, { windowId }),
diff --git a/src/containers/WorkspaceArea.js b/src/containers/WorkspaceArea.js
index c2655978c98e9a934c485eccbf5984192b2aa31a..a9f7bc2a35c800ab2216976eb342558745ff017f 100644
--- a/src/containers/WorkspaceArea.js
+++ b/src/containers/WorkspaceArea.js
@@ -14,6 +14,7 @@ import { getConfig, getWindowIds, getWorkspace } from '../state/selectors';
 const mapStateToProps = state => (
   {
     controlPanelVariant: getWorkspace(state).isWorkspaceAddVisible || getWindowIds(state).length > 0 ? undefined : 'wide',
+    isCollectionDialogVisible: getWorkspace(state).collectionDialogOn,
     isWorkspaceAddVisible: getWorkspace(state).isWorkspaceAddVisible,
     isWorkspaceControlPanelVisible: getConfig(state).workspaceControlPanel.enabled,
     lang: getConfig(state).language,
diff --git a/src/lib/CompanionWindowRegistry.js b/src/lib/CompanionWindowRegistry.js
index 5197a0af1c54aa99279320e960ccd7e5abeb1381..6ba0b1ea308fec63d3be35d9a17f6067f5cb8c4d 100644
--- a/src/lib/CompanionWindowRegistry.js
+++ b/src/lib/CompanionWindowRegistry.js
@@ -6,11 +6,13 @@ import AttributionPanel from '../containers/AttributionPanel';
 import SearchPanel from '../containers/SearchPanel';
 import LayersPanel from '../containers/LayersPanel';
 import CustomPanel from '../containers/CustomPanel';
+import WindowSideBarCollectionPanel from '../containers/WindowSideBarCollectionPanel';
 
 const map = {
   annotations: WindowSideBarAnnotationsPanel,
   attribution: AttributionPanel,
   canvas: WindowSideBarCanvasPanel,
+  collection: WindowSideBarCollectionPanel,
   custom: CustomPanel,
   info: WindowSideBarInfoPanel,
   layers: LayersPanel,
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 530decac0945dc38f381bf9549f582c23a6d9503..8c0756f50b71fda54e644df3c939ecf5356dd000 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -118,6 +118,7 @@
     "searchTitle": "Search",
     "selectWorkspaceMenu": "Select workspace type",
     "showingNumAnnotations": "Showing {{number}} annotations",
+    "showCollection": "Show collection",
     "showZoomControls": "Show zoom controls",
     "sidebarPanelsNavigation": "Sidebar panels navigation",
     "single": "Single",
@@ -129,6 +130,8 @@
     "thumbnailNavigation": "Thumbnails",
     "thumbnails": "Thumbnails",
     "toggleWindowSideBar": "Toggle sidebar",
+    "totalCollections": "{{count}} collections",
+    "totalManifests": "{{count}} manifests",
     "tryAgain": "Try again",
     "untitled": "[Untitled]",
     "view": "View",
diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js
index 0d88c9781402872b6779be6e02a0dffcdf2d9739..c1f124472712257053c1c755913de1a2e0d09099 100644
--- a/src/state/actions/action-types.js
+++ b/src/state/actions/action-types.js
@@ -70,6 +70,8 @@ const ActionTypes = {
   UPDATE_LAYERS: 'mirador/UPDATE_LAYERS',
   ADD_RESOURCE: 'mirador/ADD_RESOURCE',
   REMOVE_RESOURCE: 'mirador/REMOVE_RESOURCE',
+  SHOW_COLLECTION_DIALOG: 'mirador/SHOW_COLLECTION_DIALOG',
+  HIDE_COLLECTION_DIALOG: 'mirador/HIDE_COLLECTION_DIALOG',
 };
 
 export default ActionTypes;
diff --git a/src/state/actions/workspace.js b/src/state/actions/workspace.js
index 0d1d8241b80441b966526e70c0e8ed0eb7f5f4b3..29c52e0f1f36434d121bd983f8be8f2c660b771e 100644
--- a/src/state/actions/workspace.js
+++ b/src/state/actions/workspace.js
@@ -92,3 +92,20 @@ export function toggleDraggingEnabled() {
     type: ActionTypes.TOGGLE_DRAGGING,
   };
 }
+
+/** */
+export function showCollectionDialog(manifestId, collectionPath = [], windowId = null) {
+  return {
+    collectionPath,
+    manifestId,
+    type: ActionTypes.SHOW_COLLECTION_DIALOG,
+    windowId,
+  };
+}
+
+/** */
+export function hideCollectionDialog() {
+  return {
+    type: ActionTypes.HIDE_COLLECTION_DIALOG,
+  };
+}
diff --git a/src/state/reducers/catalog.js b/src/state/reducers/catalog.js
index d8dbc195185132bf897f508f5daa3f20675019f7..839270d0fceaf25925f563f777d8425c6487d6ca 100644
--- a/src/state/reducers/catalog.js
+++ b/src/state/reducers/catalog.js
@@ -5,7 +5,6 @@ import ActionTypes from '../actions/action-types';
  */
 export const catalogReducer = (state = [], action) => {
   switch (action.type) {
-    case ActionTypes.REQUEST_MANIFEST: // falls through, for now at least.
     case ActionTypes.ADD_RESOURCE:
       if (state.some(m => m.manifestId === action.manifestId)) return state;
 
@@ -13,6 +12,21 @@ export const catalogReducer = (state = [], action) => {
         { manifestId: action.manifestId, ...action.payload },
         ...state,
       ];
+    case ActionTypes.ADD_WINDOW:
+      if (state.some(m => m.manifestId === action.window.manifestId)) return state;
+
+      return [
+        { manifestId: action.window.manifestId },
+        ...state,
+      ];
+    case ActionTypes.UPDATE_WINDOW:
+      if (!action.payload.manifestId) return state;
+      if (state.some(m => m.manifestId === action.payload.manifestId)) return state;
+
+      return [
+        { manifestId: action.payload.manifestId },
+        ...state,
+      ];
     case ActionTypes.REMOVE_RESOURCE:
       return state.filter(r => r.manifestId !== action.manifestId);
     case ActionTypes.IMPORT_CONFIG:
diff --git a/src/state/reducers/workspace.js b/src/state/reducers/workspace.js
index 30d60e4aee6d6cc6693ff4bccfb0772296bdb46a..df6323c587537e22a5e846a2392d34575e2a3c32 100644
--- a/src/state/reducers/workspace.js
+++ b/src/state/reducers/workspace.js
@@ -99,6 +99,16 @@ export const workspaceReducer = (
       return action.state.workspace || {};
     case ActionTypes.TOGGLE_DRAGGING:
       return { ...state, draggingEnabled: !state.draggingEnabled };
+    case ActionTypes.SHOW_COLLECTION_DIALOG:
+      return {
+        ...state,
+        collectionDialogOn: true,
+        collectionManifestId: action.manifestId,
+        collectionPath: action.collectionPath,
+        collectionUpdateWindowId: action.windowId,
+      };
+    case ActionTypes.HIDE_COLLECTION_DIALOG:
+      return { ...state, collectionDialogOn: false };
     default:
       return state;
   }
diff --git a/src/state/sagas/app.js b/src/state/sagas/app.js
index ca8bd44d6027775407d475f75cafa78a3c8a6ad6..83a2c407696335ceb736469b5f1eb6a0fe8af72b 100644
--- a/src/state/sagas/app.js
+++ b/src/state/sagas/app.js
@@ -2,7 +2,7 @@ import {
   all, call, put, takeEvery,
 } from 'redux-saga/effects';
 import { v4 as uuid } from 'uuid';
-import { fetchManifest } from './iiif';
+import { fetchManifests } from './iiif';
 import { fetchWindowManifest } from './windows';
 import { addWindow } from '../actions';
 import ActionTypes from '../actions/action-types';
@@ -14,7 +14,7 @@ export function* importState(action) {
       .map(([_, window]) => call(fetchWindowManifest, { id: window.id, payload: window })),
     ...Object.entries(action.state.manifests || {})
       .filter(([_, manifest]) => !manifest.json)
-      .map(([_, manifest]) => call(fetchManifest, { manifestId: manifest.id })),
+      .map(([_, manifest]) => call(fetchManifests, manifest.id)),
   ]);
 }
 
@@ -41,10 +41,17 @@ export function* importConfig({ config: { thumbnailNavigation, windows } }) {
   yield all(thunks.map(thunk => put(thunk)));
 }
 
+/** */
+export function* fetchCollectionManifests(action) {
+  const { collectionPath, manifestId } = action;
+  yield call(fetchManifests, manifestId, ...collectionPath);
+}
+
 /** */
 export default function* appSaga() {
   yield all([
     takeEvery(ActionTypes.IMPORT_MIRADOR_STATE, importState),
     takeEvery(ActionTypes.IMPORT_CONFIG, importConfig),
+    takeEvery(ActionTypes.SHOW_COLLECTION_DIALOG, fetchCollectionManifests),
   ]);
 }
diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js
index d9d635ba70985b0f97440da373413a036486ca41..ee1f8e20599d286895b4af96b6032b0d6dd4b2f0 100644
--- a/src/state/sagas/iiif.js
+++ b/src/state/sagas/iiif.js
@@ -210,6 +210,16 @@ export function* refetchInfoResponses({ serviceId }) {
   yield put({ serviceId, type: ActionTypes.CLEAR_ACCESS_TOKEN_QUEUE });
 }
 
+/** */
+export function* fetchManifests(...manifestIds) {
+  const manifests = yield select(getManifests);
+
+  for (let i = 0; i < manifestIds.length; i += 1) {
+    const manifestId = manifestIds[i];
+    if (!manifests[manifestId]) yield call(fetchManifest, { manifestId });
+  }
+}
+
 /** */
 export default function* iiifSaga() {
   yield all([
diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js
index 6f77d0d44fed7d05ff9cf1d8be918d2584a2644c..26c8b9b1aa1ea548c0ec5db74c69629fd045361d 100644
--- a/src/state/sagas/windows.js
+++ b/src/state/sagas/windows.js
@@ -16,7 +16,7 @@ import {
 } from '../actions';
 import {
   getSearchForWindow, getSearchAnnotationsForCompanionWindow,
-  getCanvasGrouping, getWindow, getManifests, getManifestoInstance,
+  getCanvasGrouping, getWindow, getManifestoInstance,
   getCompanionWindowIdsForPosition, getManifestSearchService,
   getCanvasForAnnotation,
   getSelectedContentSearchAnnotationIds,
@@ -27,23 +27,47 @@ import {
   getCanvases,
   selectInfoResponses,
 } from '../selectors';
-import { fetchManifest } from './iiif';
+import { fetchManifests } from './iiif';
 
 /** */
 export function* fetchWindowManifest(action) {
-  const { manifestId } = action.payload || action.window;
+  const { collectionPath, manifestId } = action.payload || action.window;
 
   if (!manifestId) return;
 
   if (action.manifest) {
     yield put(receiveManifest(manifestId, action.manifest));
   } else {
-    const manifests = yield select(getManifests);
-    if (!manifests[manifestId]) yield call(fetchManifest, { manifestId });
+    yield call(fetchManifests, manifestId, ...(collectionPath || []));
   }
 
   yield call(setWindowStartingCanvas, action);
   yield call(setWindowDefaultSearchQuery, action);
+  if (!collectionPath) {
+    yield call(setCollectionPath, { manifestId, windowId: action.id || action.window.id });
+  }
+}
+
+/** */
+export function* setCollectionPath({ manifestId, windowId }) {
+  const manifestoInstance = yield select(getManifestoInstance, { manifestId });
+
+  if (manifestoInstance) {
+    const partOfs = manifestoInstance.getProperty('partOf');
+    const partOf = Array.isArray(partOfs) ? partOfs[0] : partOfs;
+
+    if (partOf && partOf.id) {
+      yield put(updateWindow(windowId, { collectionPath: [partOf.id] }));
+    }
+  }
+}
+
+/** */
+export function* fetchCollectionManifests(action) {
+  const { collectionPath } = action.payload;
+  if (!collectionPath) return;
+
+  yield call(fetchManifests, ...collectionPath);
 }
 
 /** @private */
@@ -211,6 +235,7 @@ export default function* windowsSaga() {
     takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest),
     takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas),
     takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses),
+    takeEvery(ActionTypes.UPDATE_COMPANION_WINDOW, fetchCollectionManifests),
     takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases),
     takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult),
     takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation),
diff --git a/src/state/selectors/ranges.js b/src/state/selectors/ranges.js
index 6ed958040f5c1cbf131d6f870fe9ffc8f6fcfb01..0a40388874a82176fc62b62320bf212d66510679 100644
--- a/src/state/selectors/ranges.js
+++ b/src/state/selectors/ranges.js
@@ -61,7 +61,7 @@ const getVisibleLeafAndBranchNodeIds = createSelector(
     getVisibleCanvasIds,
   ],
   (tree, canvasIds) => {
-    if (canvasIds.length === 0) return [];
+    if (canvasIds.length === 0 || !tree) return [];
     return getVisibleNodeIdsInSubTree(tree.nodes, canvasIds);
   },
 );