diff --git a/app/components/shared/Badge.js b/app/components/shared/Badge.js
index a6bd8314771ff62f5a7ce791bc311170c422cbbf..dfbaca6b82c3bfd655f04c9134dd5ec7ec79c19c 100644
--- a/app/components/shared/Badge.js
+++ b/app/components/shared/Badge.js
@@ -59,7 +59,8 @@ const label = (status, published) => {
     new: 'Unsubmitted',
     rejected: 'Rejected',
     submitted: 'Submitted',
-    revise: 'Revising',
+    revise: 'Revise',
+    revising: 'Revising',
     invited: 'Invited', // reviewer status
     completed: 'Completed', // reviewer status
   }
diff --git a/app/components/shared/ErrorBoundary.js b/app/components/shared/ErrorBoundary.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a1c54f6f9a944a14206b1fad6998426c63d6c86
--- /dev/null
+++ b/app/components/shared/ErrorBoundary.js
@@ -0,0 +1,32 @@
+/* eslint-disable class-methods-use-this */
+/* eslint-disable handle-callback-err */
+/* eslint-disable react/sort-comp */
+import React from 'react'
+
+class ErrorBoundary extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = { hasError: false }
+  }
+
+  static getDerivedStateFromError(error) {
+    // Update state so the next render will show the fallback UI.
+    return { hasError: true }
+  }
+
+  componentDidCatch(error, errorInfo) {
+    // You can also log the error to an error reporting service
+    console.error(error, errorInfo)
+  }
+
+  render() {
+    if (this.state.hasError) {
+      // You can render any custom fallback UI
+      return <h1>Something went wrong.</h1>
+    }
+
+    return this.props.children
+  }
+}
+
+export { ErrorBoundary }
diff --git a/app/components/shared/General.js b/app/components/shared/General.js
index f06d8434c3fd44c222c2114afe9981b15317d688..8bb8f942bd69e569f3f994f533409e3bd7170c42 100644
--- a/app/components/shared/General.js
+++ b/app/components/shared/General.js
@@ -49,6 +49,20 @@ export const SectionRow = styled.div`
   padding: ${grid(2)} ${grid(3)};
 `
 
+export const ClickableSectionRow = styled(SectionRow)`
+  color: ${th('colorText')};
+  :last-of-type {
+    border-radius: 0 0 ${th('borderRadius')} ${th('borderRadius')};
+  }
+  &:hover {
+    cursor: pointer;
+    background-color: ${th('colorBackgroundHue')};
+
+    svg {
+      stroke: ${th('colorPrimary')};
+    }
+  }
+`
 export const SectionRowGrid = styled(SectionRow)`
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -81,5 +95,6 @@ export { Page, Heading }
 export const HeadingWithAction = styled.div`
   display: grid;
   grid-template-columns: 1fr auto;
+  grid-gap: ${grid(2)};
   align-items: center;
 `
diff --git a/app/components/shared/Icon.jsx b/app/components/shared/Icon.jsx
index 311230387ec34741d874b7c81f37dec7963776be..171773ae63a5facde2156b28cf73348047acc62f 100644
--- a/app/components/shared/Icon.jsx
+++ b/app/components/shared/Icon.jsx
@@ -13,7 +13,7 @@ const IconWrapper = styled.div`
   position: relative;
   border-radius: 6px;
   padding: ${props => (props.noPadding || props.inline ? '0' : '8px 12px')};
-
+  top: ${props => props.top || 0};
   svg {
     stroke: ${props => props.color || props.theme.colorText};
     width: calc(${props => props.size} * ${th('gridUnit')});
@@ -28,6 +28,7 @@ export const Icon = ({
   size = 3,
   noPadding,
   inline,
+  top,
   ...props
 }) => {
   const name = _.upperFirst(_.camelCase(children))
@@ -40,6 +41,7 @@ export const Icon = ({
       noPadding={noPadding}
       role="img"
       size={size}
+      top={top}
     >
       {icons[name]({})}
     </IconWrapper>
diff --git a/app/components/shared/Select.js b/app/components/shared/Select.js
index 3f7f7a0b81fccf6e03c2f794ac0257157b94e840..0850caa8a9cbfa05724961696b3d5a250f3f3ffd 100644
--- a/app/components/shared/Select.js
+++ b/app/components/shared/Select.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-nested-ternary */
 import React, { useContext } from 'react'
 import ReactSelect from 'react-select'
 import { ThemeContext } from 'styled-components'
@@ -10,10 +11,19 @@ const styles = th => ({
 
   control: (provided, state) => ({
     ...provided,
-    border: state.isFocused
-      ? `1px solid ${th.colorPrimary}`
-      : `1px solid ${th.colorBorder}`,
-    boxShadow: state.isFocused ? `0 0 0 1px ${th.colorPrimary}` : 'none',
+    border: !state.selectProps.standalone
+      ? state.isFocused
+        ? `1px solid ${th.colorPrimary}`
+        : `1px solid ${th.colorBorder}`
+      : 'none',
+    boxShadow: !state.selectProps.standalone
+      ? state.isFocused
+        ? `0 0 0 1px ${th.colorPrimary}`
+        : 'none'
+      : state.isFocused
+      ? `0 0 0 1px ${th.colorPrimary}`
+      : th.boxShadow,
+
     borderRadius: th.borderRadius,
     '&:hover': {
       boxShadow: `0 0 0 1px ${th.colorPrimary}`,
diff --git a/app/components/shared/Tabs.js b/app/components/shared/Tabs.js
new file mode 100644
index 0000000000000000000000000000000000000000..b278d30c19added0da8513c39f3f3a06df81fa22
--- /dev/null
+++ b/app/components/shared/Tabs.js
@@ -0,0 +1,64 @@
+import React, { useState, useEffect } from 'react'
+import styled from 'styled-components'
+import { th, override } from '@pubsweet/ui-toolkit'
+
+const Tab = styled.div`
+  padding: ${th('gridUnit')} 1em;
+  font-size: ${th('fontSizeBaseSmall')};
+  font-weight: 500;
+  background-color: ${({ active }) =>
+    active ? th('colorBackground') : th('colorFurniture')};
+  border-radius: ${th('borderRadius')} ${th('borderRadius')} 0 0;
+  border-bottom: 2px solid
+    ${({ active }) => (active ? th('colorPrimary') : th('colorFurniture'))};
+  color: ${({ active }) => (active ? th('colorPrimary') : th('colorText'))};
+  cursor: pointer;
+  ${override('ui.Tab')};
+`
+
+const TabsContainer = styled.div`
+  display: flex;
+`
+
+const TabContainer = styled.div.attrs(props => ({
+  'data-test-id': props['data-test-id'] || 'tab-container',
+}))``
+
+const Content = styled.div``
+
+const Tabs = ({ sections, onChange, defaultActiveKey = null }) => {
+  const [activeKey, setActiveKey] = useState(defaultActiveKey)
+
+  useEffect(() => {
+    setActiveKey(defaultActiveKey)
+  }, [defaultActiveKey])
+
+  const setActiveKeyAndCallOnChange = activeKey => {
+    setActiveKey(activeKey)
+    if (typeof onChange === 'function') {
+      onChange(activeKey)
+    }
+  }
+
+  const currentContent = (
+    sections.find(section => section.key === activeKey) || {}
+  ).content
+  return (
+    <>
+      <TabsContainer>
+        {sections.map(({ key, label }) => (
+          <TabContainer
+            key={key}
+            onClick={() => setActiveKeyAndCallOnChange(key)}
+          >
+            <Tab active={activeKey === key}>{label || key}</Tab>
+          </TabContainer>
+        ))}
+      </TabsContainer>
+
+      {activeKey && <Content>{currentContent}</Content>}
+    </>
+  )
+}
+
+export { Tabs }
diff --git a/app/components/shared/VersionSwitcher.jsx b/app/components/shared/VersionSwitcher.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..453b3b618aa184d6ecdc6c825662a6531d3285f1
--- /dev/null
+++ b/app/components/shared/VersionSwitcher.jsx
@@ -0,0 +1,58 @@
+import React, { useState, useEffect } from 'react'
+import styled from 'styled-components'
+import { grid } from '@pubsweet/ui-toolkit'
+import { Select } from './Select'
+
+const Container = styled.div`
+  margin-top: ${props => grid(props.top)};
+`
+
+export const VersionSwitcher = ({ versions = [], children, top = 2 }) => {
+  // One can pass in versions as prop or as children
+  let normalizedVersions
+  let mode
+
+  if (versions.length) {
+    normalizedVersions = versions
+    mode = 'props'
+  } else if (children) {
+    normalizedVersions = children
+    mode = 'children'
+  }
+
+  const defaultVersion = normalizedVersions[0] && normalizedVersions[0].key
+  const [selectedVersionKey, selectVersionKey] = useState(defaultVersion)
+
+  useEffect(() => {
+    normalizedVersions = versions.length ? versions : children
+    selectVersionKey(normalizedVersions[0] && normalizedVersions[0].key)
+  }, [])
+
+  if (!normalizedVersions) {
+    return null
+  }
+
+  const selectedVersion = normalizedVersions.find(
+    v => v.key === selectedVersionKey,
+  )
+
+  return (
+    <>
+      <Select
+        onChange={option => {
+          selectVersionKey(option.value)
+        }}
+        options={normalizedVersions.map(d => ({
+          value: d.key,
+          label: mode === 'props' ? d.label : d.props.label,
+        }))}
+        placeholder="Select version..."
+        standalone
+        value={selectedVersionKey}
+      />
+      <Container top={top}>
+        {mode === 'props' ? selectedVersion.content : selectedVersion}
+      </Container>
+    </>
+  )
+}
diff --git a/app/components/shared/index.js b/app/components/shared/index.js
index beaf45b435875c15e1dd26d777a72b14deb3cd1c..94f0b4d530242ec5a2b350930c35b26bb62428ed 100644
--- a/app/components/shared/index.js
+++ b/app/components/shared/index.js
@@ -10,3 +10,6 @@ export * from './Badge'
 export * from './Select'
 export * from './Dropzone'
 export * from './FilesUpload'
+export * from './VersionSwitcher'
+export * from './Tabs'
+export * from './ErrorBoundary'
diff --git a/app/components/wax-collab/src/EditoriaLayout.js b/app/components/wax-collab/src/EditoriaLayout.js
index 1f703cc1080716ba4e41c86a2440b4feb4fd80c7..d813a25aeb4a0b93ce97dbf06fc5fb51edc13045 100644
--- a/app/components/wax-collab/src/EditoriaLayout.js
+++ b/app/components/wax-collab/src/EditoriaLayout.js
@@ -9,8 +9,9 @@ import EditorElements from './EditorElements'
 
 const Layout = styled.div`
   background-color: ${th('colorBackground')};
-  border-radius: ${th('borderRadius')};
-  max-width: 90rem;
+  border-radius: 0 ${th('borderRadius')} ${th('borderRadius')}
+    ${th('borderRadius')};
+  // max-width: 90rem;
   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
   display: grid;
 
diff --git a/app/shared/manuscript_versions.js b/app/shared/manuscript_versions.js
new file mode 100644
index 0000000000000000000000000000000000000000..f29e1741757db0d02aad6dc273085a629b1b5153
--- /dev/null
+++ b/app/shared/manuscript_versions.js
@@ -0,0 +1,26 @@
+import moment from 'moment'
+
+// TODO: memoize
+const manuscriptVersions = manuscript => {
+  const versions = []
+  if (manuscript.manuscriptVersions?.[0]) {
+    // TODO: The manuscript versions generally come ordered by
+    // created descending, but we could sort them again if need be
+    versions.push(...manuscript.manuscriptVersions)
+    versions.push(manuscript)
+  } else {
+    versions.push(manuscript)
+  }
+
+  return versions.map((manuscript, index) => ({
+    label:
+      index === 0
+        ? `Current version (${versions.length})`
+        : `${moment(manuscript.created).format(
+            'YYYY-MM-DD',
+          )} (${versions.length - index})`,
+    manuscript,
+  }))
+}
+
+export default manuscriptVersions