diff --git a/src/App.scss b/src/App.scss
index f860c16d58029983a8da71aa57f90715d92f51ff..7cb92fc9fe9c377975c5cd5810bb55df056afaa5 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -547,9 +547,21 @@ main-router {
   border-radius: var(--panelRadius, $fallback--panelRadius);
 }
 
-.panel-footer {
+/* TODO Should remove timeline-footer from here when we refactor panels into
+ * separate component and utilize slots
+ */
+.panel-footer, .timeline-footer {
+  display: flex;
   border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
   border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+  flex: none;
+  padding: 0.6em 0.6em;
+  text-align: left;
+  line-height: 28px;
+  align-items: baseline;
+  border-width: 1px 0 0 0;
+  border-style: solid;
+  border-color: var(--border, $fallback--border);
 
   .faint {
     color: $fallback--faint;
@@ -871,16 +883,10 @@ nav {
 }
 
 .new-status-notification {
-  position:relative;
-  margin-top: -1px;
+  position: relative;
   font-size: 1.1em;
-  border-width: 1px 0 0 0;
-  border-style: solid;
-  border-color: var(--border, $fallback--border);
-  padding: 10px;
   z-index: 1;
-  background-color: $fallback--fg;
-  background-color: var(--panel, $fallback--fg);
+  flex: 1;
 }
 
 .chat-layout {
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
deleted file mode 100644
index 8ffe34f8fb7c16e9413c569b7a86f979723dfc29..0000000000000000000000000000000000000000
--- a/src/components/export_import/export_import.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-  <div class="import-export-container">
-    <slot name="before" />
-    <button
-      class="btn button-default"
-      @click="exportData"
-    >
-      {{ exportLabel }}
-    </button>
-    <button
-      class="btn button-default"
-      @click="importData"
-    >
-      {{ importLabel }}
-    </button>
-    <slot name="afterButtons" />
-    <p
-      v-if="importFailed"
-      class="alert error"
-    >
-      {{ importFailedText }}
-    </p>
-    <slot name="afterError" />
-  </div>
-</template>
-
-<script>
-export default {
-  props: [
-    'exportObject',
-    'importLabel',
-    'exportLabel',
-    'importFailedText',
-    'validator',
-    'onImport',
-    'onImportFailure'
-  ],
-  data () {
-    return {
-      importFailed: false
-    }
-  },
-  methods: {
-    exportData () {
-      const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
-
-      // Create an invisible link with a data url and simulate a click
-      const e = document.createElement('a')
-      e.setAttribute('download', 'pleroma_theme.json')
-      e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
-      e.style.display = 'none'
-
-      document.body.appendChild(e)
-      e.click()
-      document.body.removeChild(e)
-    },
-    importData () {
-      this.importFailed = false
-      const filePicker = document.createElement('input')
-      filePicker.setAttribute('type', 'file')
-      filePicker.setAttribute('accept', '.json')
-
-      filePicker.addEventListener('change', event => {
-        if (event.target.files[0]) {
-          // eslint-disable-next-line no-undef
-          const reader = new FileReader()
-          reader.onload = ({ target }) => {
-            try {
-              const parsed = JSON.parse(target.result)
-              const valid = this.validator(parsed)
-              if (valid) {
-                this.onImport(parsed)
-              } else {
-                this.importFailed = true
-                // this.onImportFailure(valid)
-              }
-            } catch (e) {
-              // This will happen both if there is a JSON syntax error or the theme is missing components
-              this.importFailed = true
-              // this.onImportFailure(e)
-            }
-          }
-          reader.readAsText(event.target.files[0])
-        }
-      })
-
-      document.body.appendChild(filePicker)
-      filePicker.click()
-      document.body.removeChild(filePicker)
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-.import-export-container {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: baseline;
-  justify-content: center;
-}
-</style>
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 682ae127d1748ae8ffa003d1e5cf77238429e17a..2bb627a8c92e6852ff08437263776104ee545cdf 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -1,6 +1,6 @@
 @import '../../_variables.scss';
 
-.notifications {
+.Notifications {
   &:not(.minimal) {
     // a bit of a hack to allow scrolling below notifications
     padding-bottom: 15em;
@@ -11,6 +11,10 @@
     color: var(--text, $fallback--text);
   }
 
+  .notifications-footer {
+    border: none;
+  }
+
   .notification {
     position: relative;
 
@@ -82,7 +86,6 @@
     }
   }
 
-
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 12097b856edb8a5aabcf0f6102e734aee1776205..2ce5d56f7e3a4ae1c664a70130d6dee1c64a67f3 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,7 +1,7 @@
 <template>
   <div
     :class="{ minimal: minimalMode }"
-    class="notifications"
+    class="Notifications"
   >
     <div :class="mainClass">
       <div
@@ -35,10 +35,10 @@
           <notification :notification="notification" />
         </div>
       </div>
-      <div class="panel-footer">
+      <div class="panel-footer notifications-footer">
         <div
           v-if="bottomedOut"
-          class="new-status-notification text-center panel-footer faint"
+          class="new-status-notification text-center faint"
         >
           {{ $t('notifications.no_more_notifications') }}
         </div>
@@ -47,13 +47,13 @@
           class="button-unstyled -link -fullwidth"
           @click.prevent="fetchOlderNotifications()"
         >
-          <div class="new-status-notification text-center panel-footer">
+          <div class="new-status-notification text-center">
             {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
           </div>
         </button>
         <div
           v-else
-          class="new-status-notification text-center panel-footer"
+          class="new-status-notification text-center"
         >
           <FAIcon
             icon="circle-notch"
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index f0d49c91587b4ac32f960815c577663154621715..0404348334859659f66454cfc4fc6d702c572b6a 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -2,10 +2,55 @@ import Modal from 'src/components/modal/modal.vue'
 import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
 import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
 import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
+import Popover from '../popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { cloneDeep } from 'lodash'
+import {
+  newImporter,
+  newExporter
+} from 'src/services/export_import/export_import.js'
+import {
+  faTimes,
+  faFileUpload,
+  faFileDownload,
+  faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+import {
+  faWindowMinimize
+} from '@fortawesome/free-regular-svg-icons'
+
+const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1
+const PLEROMAFE_SETTINGS_MINOR_VERSION = 0
+
+library.add(
+  faTimes,
+  faWindowMinimize,
+  faFileUpload,
+  faFileDownload,
+  faChevronDown
+)
 
 const SettingsModal = {
+  data () {
+    return {
+      dataImporter: newImporter({
+        validator: this.importValidator,
+        onImport: this.onImport,
+        onImportFailure: this.onImportFailure
+      }),
+      dataThemeExporter: newExporter({
+        filename: 'pleromafe_settings.full',
+        getExportedObject: () => this.generateExport(true)
+      }),
+      dataExporter: newExporter({
+        filename: 'pleromafe_settings',
+        getExportedObject: () => this.generateExport()
+      })
+    }
+  },
   components: {
     Modal,
+    Popover,
     SettingsModalContent: getResettableAsyncComponent(
       () => import('./settings_modal_content.vue'),
       {
@@ -21,6 +66,85 @@ const SettingsModal = {
     },
     peekModal () {
       this.$store.dispatch('togglePeekSettingsModal')
+    },
+    importValidator (data) {
+      if (!Array.isArray(data._pleroma_settings_version)) {
+        return {
+          messageKey: 'settings.file_import_export.invalid_file'
+        }
+      }
+
+      const [major, minor] = data._pleroma_settings_version
+
+      if (major > PLEROMAFE_SETTINGS_MAJOR_VERSION) {
+        return {
+          messageKey: 'settings.file_export_import.errors.file_too_new',
+          messageArgs: {
+            fileMajor: major,
+            feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION
+          }
+        }
+      }
+
+      if (major < PLEROMAFE_SETTINGS_MAJOR_VERSION) {
+        return {
+          messageKey: 'settings.file_export_import.errors.file_too_old',
+          messageArgs: {
+            fileMajor: major,
+            feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION
+          }
+        }
+      }
+
+      if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) {
+        this.$store.dispatch('pushGlobalNotice', {
+          level: 'warning',
+          messageKey: 'settings.file_export_import.errors.file_slightly_new'
+        })
+      }
+
+      return true
+    },
+    onImportFailure (result) {
+      if (result.error) {
+        this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_settings_imported', level: 'error' })
+      } else {
+        this.$store.dispatch('pushGlobalNotice', { ...result.validationResult, level: 'error' })
+      }
+    },
+    onImport (data) {
+      if (data) { this.$store.dispatch('loadSettings', data) }
+    },
+    restore () {
+      this.dataImporter.importData()
+    },
+    backup () {
+      this.dataExporter.exportData()
+    },
+    backupWithTheme () {
+      this.dataThemeExporter.exportData()
+    },
+    generateExport (theme = false) {
+      const { config } = this.$store.state
+      let sample = config
+      if (!theme) {
+        const ignoreList = new Set([
+          'customTheme',
+          'customThemeSource',
+          'colors'
+        ])
+        sample = Object.fromEntries(
+          Object
+            .entries(sample)
+            .filter(([key]) => !ignoreList.has(key))
+        )
+      }
+      const clone = cloneDeep(sample)
+      clone._pleroma_settings_version = [
+        PLEROMAFE_SETTINGS_MAJOR_VERSION,
+        PLEROMAFE_SETTINGS_MINOR_VERSION
+      ]
+      return clone
     }
   },
   computed: {
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 552ca41f947a5b3ac2a2754f84815d598bbfc4c5..c7da54335829c8132133be175d386c61a374d28f 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -31,20 +31,86 @@
         </transition>
         <button
           class="btn button-default"
+          :title="$t('general.peek')"
           @click="peekModal"
         >
-          {{ $t('general.peek') }}
+          <FAIcon
+            :icon="['far', 'window-minimize']"
+            fixed-width
+          />
         </button>
         <button
           class="btn button-default"
+          :title="$t('general.close')"
           @click="closeModal"
         >
-          {{ $t('general.close') }}
+          <FAIcon
+            icon="times"
+            fixed-width
+          />
         </button>
       </div>
       <div class="panel-body">
         <SettingsModalContent v-if="modalOpenedOnce" />
       </div>
+      <div class="panel-footer">
+        <Popover
+          class="export"
+          trigger="click"
+          placement="top"
+          :offset="{ y: 5, x: 5 }"
+          :bound-to="{ x: 'container' }"
+          remove-padding
+        >
+          <button
+            slot="trigger"
+            class="btn button-default"
+            :title="$t('general.close')"
+          >
+            <span>{{ $t("settings.file_export_import.backup_restore") }}</span>
+            <FAIcon
+              icon="chevron-down"
+            />
+          </button>
+          <div
+            slot="content"
+            slot-scope="{close}"
+          >
+            <div class="dropdown-menu">
+              <button
+                class="button-default dropdown-item dropdown-item-icon"
+                @click.prevent="backup"
+                @click="close"
+              >
+                <FAIcon
+                  icon="file-download"
+                  fixed-width
+                /><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
+              </button>
+              <button
+                class="button-default dropdown-item dropdown-item-icon"
+                @click.prevent="backupWithTheme"
+                @click="close"
+              >
+                <FAIcon
+                  icon="file-download"
+                  fixed-width
+                /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
+              </button>
+              <button
+                class="button-default dropdown-item dropdown-item-icon"
+                @click.prevent="restore"
+                @click="close"
+              >
+                <FAIcon
+                  icon="file-upload"
+                  fixed-width
+                /><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
+              </button>
+            </div>
+          </div>
+        </Popover>
+      </div>
     </div>
   </Modal>
 </template>
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 6cf75fe7102414f1067ca8f6a592e91a3fbf1c12..8960c566517fc7dad379d4b8cfb90747babef3a2 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -15,6 +15,10 @@ import {
   shadows2to3,
   colors2to3
 } from 'src/services/style_setter/style_setter.js'
+import {
+  newImporter,
+  newExporter
+} from 'src/services/export_import/export_import.js'
 import {
   SLOT_INHERITANCE
 } from 'src/services/theme_data/pleromafe.js'
@@ -31,7 +35,6 @@ import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
 import FontControl from 'src/components/font_control/font_control.vue'
 import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
 import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
-import ExportImport from 'src/components/export_import/export_import.vue'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
 
 import Preview from './preview.vue'
@@ -67,6 +70,15 @@ const colorConvert = (color) => {
 export default {
   data () {
     return {
+      themeImporter: newImporter({
+        validator: this.importValidator,
+        onImport: this.onImport,
+        onImportFailure: this.onImportFailure
+      }),
+      themeExporter: newExporter({
+        filename: 'pleroma_theme',
+        getExportedObject: () => this.exportedTheme
+      }),
       availableStyles: [],
       selected: this.$store.getters.mergedConfig.theme,
       themeWarning: undefined,
@@ -383,7 +395,6 @@ export default {
     FontControl,
     TabSwitcher,
     Preview,
-    ExportImport,
     Checkbox
   },
   methods: {
@@ -528,10 +539,15 @@ export default {
         this.previewColors.mod
       )
     },
+    importTheme () { this.themeImporter.importData() },
+    exportTheme () { this.themeExporter.exportData() },
     onImport (parsed, forceSource = false) {
       this.tempImportFile = parsed
       this.loadTheme(parsed, 'file', forceSource)
     },
+    onImportFailure (result) {
+      this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
+    },
     importValidator (parsed) {
       const version = parsed._pleroma_theme_version
       return version >= 1 || version <= 2
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index b8add42f48973d2dba28e6faf8e46461f39a5997..6237886708716b54c9002f6c9a21e62ba0e82cec 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -48,46 +48,51 @@
             </template>
           </div>
         </div>
-        <ExportImport
-          :export-object="exportedTheme"
-          :export-label="$t(&quot;settings.export_theme&quot;)"
-          :import-label="$t(&quot;settings.import_theme&quot;)"
-          :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
-          :on-import="onImport"
-          :validator="importValidator"
-        >
-          <template slot="before">
-            <div class="presets">
-              {{ $t('settings.presets') }}
-              <label
-                for="preset-switcher"
-                class="select"
+        <div class="top">
+          <div class="presets">
+            {{ $t('settings.presets') }}
+            <label
+              for="preset-switcher"
+              class="select"
+            >
+              <select
+                id="preset-switcher"
+                v-model="selected"
+                class="preset-switcher"
               >
-                <select
-                  id="preset-switcher"
-                  v-model="selected"
-                  class="preset-switcher"
+                <option
+                  v-for="style in availableStyles"
+                  :key="style.name"
+                  :value="style"
+                  :style="{
+                    backgroundColor: style[1] || (style.theme || style.source).colors.bg,
+                    color: style[3] || (style.theme || style.source).colors.text
+                  }"
                 >
-                  <option
-                    v-for="style in availableStyles"
-                    :key="style.name"
-                    :value="style"
-                    :style="{
-                      backgroundColor: style[1] || (style.theme || style.source).colors.bg,
-                      color: style[3] || (style.theme || style.source).colors.text
-                    }"
-                  >
-                    {{ style[0] || style.name }}
-                  </option>
-                </select>
-                <FAIcon
-                  class="select-down-icon"
-                  icon="chevron-down"
-                />
-              </label>
-            </div>
-          </template>
-        </ExportImport>
+                  {{ style[0] || style.name }}
+                </option>
+              </select>
+              <FAIcon
+                class="select-down-icon"
+                icon="chevron-down"
+              />
+            </label>
+          </div>
+          <div class="export-import">
+            <button
+              class="btn button-default"
+              @click="importTheme"
+            >
+              {{ $t(&quot;settings.import_theme&quot;) }}
+            </button>
+            <button
+              class="btn button-default"
+              @click="exportTheme"
+            >
+              {{ $t(&quot;settings.export_theme&quot;) }}
+            </button>
+          </div>
+        </div>
       </div>
       <div class="save-load-options">
         <span class="keep-option">
diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2c5a67e2b73fa5bb24fb50cf1ff1c9336562d3aa
--- /dev/null
+++ b/src/components/timeline/timeline.scss
@@ -0,0 +1,31 @@
+@import '../../_variables.scss';
+
+.Timeline {
+  .loadmore-text {
+    opacity: 1;
+  }
+
+  &.-blocked {
+    cursor: progress;
+  }
+
+  .timeline-heading {
+    max-width: 100%;
+    flex-wrap: nowrap;
+    align-items: center;
+    position: relative;
+
+    .loadmore-button {
+      flex-shrink: 0;
+    }
+
+    .loadmore-text {
+      flex-shrink: 0;
+      line-height: 1em;
+    }
+  }
+
+  .timeline-footer {
+    border: none;
+  }
+}
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 286477c25ee25c36e96ecc76f87a0f8d7b4ebd03..767428f062575df914ca6eafea5a1b451476462f 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -52,13 +52,13 @@
     <div :class="classes.footer">
       <div
         v-if="count===0"
-        class="new-status-notification text-center panel-footer faint"
+        class="new-status-notification text-center faint"
       >
         {{ $t('timeline.no_statuses') }}
       </div>
       <div
         v-else-if="bottomedOut"
-        class="new-status-notification text-center panel-footer faint"
+        class="new-status-notification text-center faint"
       >
         {{ $t('timeline.no_more_statuses') }}
       </div>
@@ -67,13 +67,13 @@
         class="button-unstyled -link -fullwidth"
         @click.prevent="fetchOlderStatuses()"
       >
-        <div class="new-status-notification text-center panel-footer">
+        <div class="new-status-notification text-center">
           {{ $t('timeline.load_older') }}
         </div>
       </button>
       <div
         v-else
-        class="new-status-notification text-center panel-footer"
+        class="new-status-notification text-center"
       >
         <FAIcon
           icon="circle-notch"
@@ -87,32 +87,4 @@
 
 <script src="./timeline.js"></script>
 
-<style lang="scss">
-@import '../../_variables.scss';
-
-.Timeline {
-  .loadmore-text {
-    opacity: 1;
-  }
-
-  &.-blocked {
-    cursor: progress;
-  }
-}
-
-.timeline-heading {
-  max-width: 100%;
-  flex-wrap: nowrap;
-  align-items: center;
-  position: relative;
-
-  .loadmore-button {
-    flex-shrink: 0;
-  }
-
-  .loadmore-text {
-    flex-shrink: 0;
-    line-height: 1em;
-  }
-}
-</style>
+<style src="./timeline.scss" lang="scss"> </style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 471098eb961c9185301d9e0a7eb00b9fa8346039..d613848269949e5bd6523f79fceb24f449185e1f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -368,6 +368,18 @@
     "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
     "mutes_tab": "Mutes",
     "play_videos_in_modal": "Play videos in a popup frame",
+    "file_export_import": {
+      "backup_restore": "Settings backup",
+      "backup_settings": "Backup settings to file",
+      "backup_settings_theme": "Backup settings and theme to file",
+      "restore_settings": "Restore settings from file",
+      "errors": {
+        "invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made.",
+        "file_too_new": "Incompatile major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it",
+        "file_too_old": "Incompatile major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})",
+        "file_slightly_new": "File minor version is different, some settings might not load"
+      }
+    },
     "profile_fields": {
       "label": "Profile metadata",
       "add_field": "Add field",
diff --git a/src/modules/config.js b/src/modules/config.js
index f98e6649614ac4975e1d371711c3b528f1d39116..629588a84ef8477b71831a479de87b2591752114 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -110,6 +110,20 @@ const config = {
     }
   },
   actions: {
+    loadSettings ({ dispatch }, data) {
+      const knownKeys = new Set(Object.keys(defaultState))
+      const presentKeys = new Set(Object.keys(data))
+      const intersection = new Set()
+      for (let elem of presentKeys) {
+        if (knownKeys.has(elem)) {
+          intersection.add(elem)
+        }
+      }
+
+      intersection.forEach(
+        name => dispatch('setOption', { name, value: data[name] })
+      )
+    },
     setHighlight ({ commit, dispatch }, { user, color, type }) {
       commit('setHighlight', { user, color, type })
     },
diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac67cf9c528387112f10bc00df3972901d898643
--- /dev/null
+++ b/src/services/export_import/export_import.js
@@ -0,0 +1,55 @@
+export const newExporter = ({
+  filename = 'data',
+  getExportedObject
+}) => ({
+  exportData () {
+    const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces
+
+    // Create an invisible link with a data url and simulate a click
+    const e = document.createElement('a')
+    e.setAttribute('download', `${filename}.json`)
+    e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
+    e.style.display = 'none'
+
+    document.body.appendChild(e)
+    e.click()
+    document.body.removeChild(e)
+  }
+})
+
+export const newImporter = ({
+  onImport,
+  onImportFailure,
+  validator = () => true
+}) => ({
+  importData () {
+    const filePicker = document.createElement('input')
+    filePicker.setAttribute('type', 'file')
+    filePicker.setAttribute('accept', '.json')
+
+    filePicker.addEventListener('change', event => {
+      if (event.target.files[0]) {
+        // eslint-disable-next-line no-undef
+        const reader = new FileReader()
+        reader.onload = ({ target }) => {
+          try {
+            const parsed = JSON.parse(target.result)
+            const validationResult = validator(parsed)
+            if (validationResult === true) {
+              onImport(parsed)
+            } else {
+              onImportFailure({ validationResult })
+            }
+          } catch (error) {
+            onImportFailure({ error })
+          }
+        }
+        reader.readAsText(event.target.files[0])
+      }
+    })
+
+    document.body.appendChild(filePicker)
+    filePicker.click()
+    document.body.removeChild(filePicker)
+  }
+})
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index a2bba67bcee067733752d5559fe738d40f6336fb..f75e69168f7c7c63253b6251413496e91c0905a7 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -380,7 +380,7 @@ export const colors2to3 = (colors) => {
  */
 export const shadows2to3 = (shadows, opacity) => {
   return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
-    const isDynamic = ({ color }) => color.startsWith('--')
+    const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
     const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
     const newShadow = shadowDefs.reduce((shadowAcc, def) => [
       ...shadowAcc,