diff --git a/__tests__/AnnotationCreation.test.js b/__tests__/AnnotationCreation.test.js
index b3bfeb053dec48de7da38712f0bcc5bf5be2e867..5f79a8291da2447385a5c2ff82ac5120400d13bf 100644
--- a/__tests__/AnnotationCreation.test.js
+++ b/__tests__/AnnotationCreation.test.js
@@ -36,4 +36,16 @@ describe('AnnotationCreation', () => {
     wrapper = createWrapper();
     expect(wrapper.dive().find(TextEditor).length).toBe(1);
   });
+  it('can handle annotations without target selector', () => {
+    wrapper = createWrapper({
+      annotation: {
+        body: {
+          purpose: 'commenting',
+          value: 'Foo bar',
+        },
+        target: {},
+      },
+    });
+    wrapper.dive();
+  });
 });
diff --git a/__tests__/AnnotationExportDialog.test.js b/__tests__/AnnotationExportDialog.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..21d9e5c2373c5be35675ce29ddd74e684e605bdc
--- /dev/null
+++ b/__tests__/AnnotationExportDialog.test.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import MenuItem from '@material-ui/core/MenuItem';
+import AnnotationExportDialog from '../src/AnnotationExportDialog';
+
+window.URL.createObjectURL = jest.fn((data) => ('downloadurl'));
+
+const adapter = jest.fn(() => (
+  {
+    all: jest.fn().mockResolvedValue(
+      {
+        id: 'pageId/3',
+        items: [
+          { id: 'anno/2' },
+        ],
+        type: 'AnnotationPage',
+      },
+    ),
+    annotationPageId: 'pageId/3',
+  }
+));
+
+/** */
+function createWrapper(props) {
+  return shallow(
+    <AnnotationExportDialog
+      canvases={[]}
+      config={{ annotation: { adapter } }}
+      handleClose={jest.fn()}
+      open
+      {...props}
+    />,
+  );
+}
+
+describe('AnnotationExportDialog', () => {
+  it('renders download link for every annotation page', async () => {
+    let wrapper = createWrapper({
+      canvases: [
+        { id: 'canvas/1' },
+        { id: 'canvas/2' },
+      ],
+    }).dive();
+    expect(wrapper.text()).toEqual(expect.stringContaining('No annotations stored yet.'));
+
+    wrapper.instance().componentDidUpdate({ open: false });
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    wrapper = wrapper.update();
+    expect(wrapper.text()).toEqual(expect.not.stringContaining('No annotations stored yet.'));
+    expect(wrapper.find(MenuItem).some({ 'aria-label': 'Export annotations for canvas/1' })).toBe(true);
+    expect(wrapper.find(MenuItem).some({ 'aria-label': 'Export annotations for canvas/2' })).toBe(true);
+  });
+});
diff --git a/__tests__/miradorAnnotationPlugin.test.js b/__tests__/miradorAnnotationPlugin.test.js
index c9402a77a906e01ae33572a4fd26ea6575b58495..2ea9032469f3da38279c6c0dc4c11b316ed62ab9 100644
--- a/__tests__/miradorAnnotationPlugin.test.js
+++ b/__tests__/miradorAnnotationPlugin.test.js
@@ -1,12 +1,14 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import { MiradorMenuButton } from 'mirador/dist/es/src/components/MiradorMenuButton';
+import LocalStorageAdapter from '../src/LocalStorageAdapter';
 import miradorAnnotationPlugin from '../src/plugins/miradorAnnotationPlugin';
 
 /** */
 function createWrapper(props) {
   return shallow(
     <miradorAnnotationPlugin.component
+      canvases={[]}
       config={{}}
       TargetComponent="<div>hello</div>"
       targetProps={{}}
@@ -48,4 +50,38 @@ describe('MiradorAnnotation', () => {
     wrapper.find(MiradorMenuButton).simulate('click');
     expect(wrapper.instance().state.singleCanvasDialogOpen).toBe(true);
   });
+  it('renders no export button if export or LocalStorageAdapter are not configured', () => {
+    wrapper = createWrapper();
+    expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
+
+    wrapper = createWrapper({
+      config: {
+        annotation: {
+          adapter: () => () => {},
+          exportLocalStorageAnnotations: true,
+        },
+      },
+    });
+    expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
+
+    wrapper = createWrapper({
+      config: {
+        annotation: {
+          adapter: (canvasId) => new LocalStorageAdapter(`test://?canvasId=${canvasId}`),
+        },
+      },
+    });
+    expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
+  });
+  it('renders export button if export and LocalStorageAdapter are configured', () => {
+    wrapper = createWrapper({
+      config: {
+        annotation: {
+          adapter: (canvasId) => new LocalStorageAdapter(`test://?canvasId=${canvasId}`),
+          exportLocalStorageAnnotations: true,
+        },
+      },
+    });
+    expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(true);
+  });
 });
diff --git a/demo/src/index.js b/demo/src/index.js
index 3f7793bdd0274e9d20e8096a9784c9ee96fa847c..ef6831b6f8582d6d933a54005469f9c38414986d 100644
--- a/demo/src/index.js
+++ b/demo/src/index.js
@@ -9,6 +9,7 @@ const config = {
   annotation: {
     adapter: (canvasId) => new LocalStorageAdapter(`localStorage://?canvasId=${canvasId}`),
     // adapter: (canvasId) => new AnnototAdapter(canvasId, endpointUrl),
+    exportLocalStorageAnnotations: false, // display annotation JSON export button
   },
   id: 'demo',
   window: {
diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js
index c58f4452c61aa3ecdb7e05ae82b8a0d31dceedd2..54d09c628acf4da5703376bc93a83887a3584e21 100644
--- a/src/AnnotationCreation.js
+++ b/src/AnnotationCreation.js
@@ -50,16 +50,18 @@ class AnnotationCreation extends Component {
       } else {
         annoState.annoBody = props.annotation.body.value;
       }
-      if (Array.isArray(props.annotation.target.selector)) {
-        props.annotation.target.selector.forEach((selector) => {
-          if (selector.type === 'SvgSelector') {
-            annoState.svg = selector.value;
-          } else if (selector.type === 'FragmentSelector') {
-            annoState.xywh = selector.value.replace('xywh=', '');
-          }
-        });
-      } else {
-        annoState.svg = props.annotation.target.selector.value;
+      if (props.annotation.target.selector) {
+        if (Array.isArray(props.annotation.target.selector)) {
+          props.annotation.target.selector.forEach((selector) => {
+            if (selector.type === 'SvgSelector') {
+              annoState.svg = selector.value;
+            } else if (selector.type === 'FragmentSelector') {
+              annoState.xywh = selector.value.replace('xywh=', '');
+            }
+          });
+        } else {
+          annoState.svg = props.annotation.target.selector.value;
+        }
       }
     }
     this.state = {
diff --git a/src/AnnotationExportDialog.js b/src/AnnotationExportDialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..9202209b55518038cf67acb5cd3b40c0b973ccaa
--- /dev/null
+++ b/src/AnnotationExportDialog.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react';
+import Dialog from '@material-ui/core/Dialog';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import GetAppIcon from '@material-ui/icons/GetApp';
+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 PropTypes, { bool } from 'prop-types';
+import { withStyles } from '@material-ui/core';
+
+/** */
+const styles = (theme) => ({
+  listitem: {
+    '&:focus': {
+      backgroundColor: theme.palette.action.focus,
+    },
+    '&:hover': {
+      backgroundColor: theme.palette.action.hover,
+    },
+  },
+});
+
+/** */
+class AnnotationExportDialog extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+    this.state = {
+      exportLinks: [],
+    };
+    this.closeDialog = this.closeDialog.bind(this);
+  }
+
+  /** */
+  componentDidUpdate(prevProps) {
+    const { canvases, config, open } = this.props;
+    const { open: prevOpen } = prevProps || {};
+    if (prevOpen !== open && open) {
+      /** */
+      const reducer = async (acc, canvas) => {
+        const store = config.annotation.adapter(canvas.id);
+        const resolvedAcc = await acc;
+        const content = await store.all();
+        if (content) {
+          // eslint-disable-next-line no-underscore-dangle
+          const label = (canvas.__jsonld && canvas.__jsonld.label) || canvas.id;
+          const data = new Blob([JSON.stringify(content)], { type: 'application/json' });
+          const url = window.URL.createObjectURL(data);
+          return [...resolvedAcc, {
+            canvasId: canvas.id,
+            id: content.id || content['@id'],
+            label,
+            url,
+          }];
+        }
+        return resolvedAcc;
+      };
+      if (canvases && canvases.length > 0) {
+        canvases.reduce(reducer, []).then((exportLinks) => {
+          this.setState({ exportLinks });
+        });
+      }
+    }
+  }
+
+  /** */
+  closeDialog() {
+    const { handleClose } = this.props;
+    this.setState({ exportLinks: [] });
+    handleClose();
+  }
+
+  /** */
+  render() {
+    const { classes, handleClose, open } = this.props;
+    const { exportLinks } = this.state;
+    return (
+      <Dialog
+        aria-labelledby="annotation-export-dialog-title"
+        id="annotation-export-dialog"
+        onClose={handleClose}
+        onEscapeKeyDown={this.closeDialog}
+        open={open}
+      >
+        <DialogTitle id="annotation-export-dialog-title" disableTypography>
+          <Typography variant="h2">Export Annotations</Typography>
+        </DialogTitle>
+        <DialogContent>
+          { exportLinks === undefined || exportLinks.length === 0 ? (
+            <Typography variant="body1">No annotations stored yet.</Typography>
+          ) : (
+            <MenuList>
+              { exportLinks.map((dl) => (
+                <MenuItem
+                  button
+                  className={classes.listitem}
+                  component="a"
+                  key={dl.canvasId}
+                  aria-label={`Export annotations for ${dl.label}`}
+                  href={dl.url}
+                  download={`${dl.id}.json`}
+                >
+                  <ListItemIcon>
+                    <GetAppIcon />
+                  </ListItemIcon>
+                  <ListItemText>
+                    {`Export annotations for "${dl.label}"`}
+                  </ListItemText>
+                </MenuItem>
+              ))}
+            </MenuList>
+          )}
+        </DialogContent>
+      </Dialog>
+    );
+  }
+}
+
+AnnotationExportDialog.propTypes = {
+  canvases: PropTypes.arrayOf(
+    PropTypes.shape({ id: PropTypes.string }),
+  ).isRequired,
+  classes: PropTypes.objectOf(PropTypes.string),
+  config: PropTypes.shape({
+    annotation: PropTypes.shape({
+      adapter: PropTypes.func,
+    }),
+  }).isRequired,
+  handleClose: PropTypes.func.isRequired,
+  open: bool.isRequired,
+};
+
+AnnotationExportDialog.defaultProps = {
+  classes: {},
+};
+
+export default withStyles(styles)(AnnotationExportDialog);
diff --git a/src/plugins/miradorAnnotationPlugin.js b/src/plugins/miradorAnnotationPlugin.js
index 17b09dce48833f0b46f84d83498722bbfa5d5a6b..e3a06fc3ddb7200ff602bc605ca96e28fa8c8f45 100644
--- a/src/plugins/miradorAnnotationPlugin.js
+++ b/src/plugins/miradorAnnotationPlugin.js
@@ -3,8 +3,12 @@ import PropTypes from 'prop-types';
 import * as actions from 'mirador/dist/es/src/state/actions';
 import { getWindowViewType } from 'mirador/dist/es/src/state/selectors';
 import AddBoxIcon from '@material-ui/icons/AddBox';
+import GetAppIcon from '@material-ui/icons/GetApp';
 import { MiradorMenuButton } from 'mirador/dist/es/src/components/MiradorMenuButton';
+import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
 import SingleCanvasDialog from '../SingleCanvasDialog';
+import AnnotationExportDialog from '../AnnotationExportDialog';
+import LocalStorageAdapter from '../LocalStorageAdapter';
 
 /** */
 class MiradorAnnotation extends Component {
@@ -12,9 +16,11 @@ class MiradorAnnotation extends Component {
   constructor(props) {
     super(props);
     this.state = {
+      annotationExportDialogOpen: false,
       singleCanvasDialogOpen: false,
     };
     this.openCreateAnnotationCompanionWindow = this.openCreateAnnotationCompanionWindow.bind(this);
+    this.toggleCanvasExportDialog = this.toggleCanvasExportDialog.bind(this);
     this.toggleSingleCanvasDialogOpen = this.toggleSingleCanvasDialogOpen.bind(this);
   }
 
@@ -37,15 +43,29 @@ class MiradorAnnotation extends Component {
     });
   }
 
+  /** */
+  toggleCanvasExportDialog(e) {
+    const { annotationExportDialogOpen } = this.state;
+    const newState = {
+      annotationExportDialogOpen: !annotationExportDialogOpen,
+    };
+    this.setState(newState);
+  }
+
   /** */
   render() {
     const {
+      canvases,
+      config,
       switchToSingleCanvasView,
       TargetComponent,
       targetProps,
       windowViewType,
     } = this.props;
-    const { singleCanvasDialogOpen } = this.state;
+    const { annotationExportDialogOpen, singleCanvasDialogOpen } = this.state;
+    const storageAdapter = config.annotation && config.annotation.adapter('poke');
+    const offerExportDialog = config.annotation && storageAdapter instanceof LocalStorageAdapter
+      && config.annotation.exportLocalStorageAnnotations;
     return (
       <div>
         <TargetComponent
@@ -58,15 +78,30 @@ class MiradorAnnotation extends Component {
         >
           <AddBoxIcon />
         </MiradorMenuButton>
-        {
-          singleCanvasDialogOpen && (
-            <SingleCanvasDialog
-              open={singleCanvasDialogOpen}
-              handleClose={this.toggleSingleCanvasDialogOpen}
-              switchToSingleCanvasView={switchToSingleCanvasView}
-            />
-          )
-        }
+        { singleCanvasDialogOpen && (
+          <SingleCanvasDialog
+            open={singleCanvasDialogOpen}
+            handleClose={this.toggleSingleCanvasDialogOpen}
+            switchToSingleCanvasView={switchToSingleCanvasView}
+          />
+        )}
+        { offerExportDialog && (
+          <MiradorMenuButton
+            aria-label="Export local annotations for visible items"
+            onClick={this.toggleCanvasExportDialog}
+            size="small"
+          >
+            <GetAppIcon />
+          </MiradorMenuButton>
+        )}
+        { offerExportDialog && (
+          <AnnotationExportDialog
+            canvases={canvases}
+            config={config}
+            handleClose={this.toggleCanvasExportDialog}
+            open={annotationExportDialogOpen}
+          />
+        )}
       </div>
     );
   }
@@ -74,6 +109,15 @@ class MiradorAnnotation extends Component {
 
 MiradorAnnotation.propTypes = {
   addCompanionWindow: PropTypes.func.isRequired,
+  canvases: PropTypes.arrayOf(
+    PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }),
+  ).isRequired,
+  config: PropTypes.shape({
+    annotation: PropTypes.shape({
+      adapter: PropTypes.func,
+      exportLocalStorageAnnotations: PropTypes.bool,
+    }),
+  }).isRequired,
   switchToSingleCanvasView: PropTypes.func.isRequired,
   TargetComponent: PropTypes.oneOfType([
     PropTypes.func,
@@ -94,8 +138,10 @@ const mapDispatchToProps = (dispatch, props) => ({
 });
 
 /** */
-const mapStateToProps = (state, props) => ({
-  windowViewType: getWindowViewType(state, { windowId: props.targetProps.windowId }),
+const mapStateToProps = (state, { targetProps: { windowId } }) => ({
+  canvases: getVisibleCanvases(state, { windowId }),
+  config: state.config,
+  windowViewType: getWindowViewType(state, { windowId }),
 });
 
 export default {