diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc
index a8db38407..7d5cb5e51 100644
--- a/common/src/app/common/colors.cljc
+++ b/common/src/app/common/colors.cljc
@@ -10,10 +10,13 @@
 (def black "#000000")
 (def canvas "#E8E9EA")
 (def default-layout "#DE4762")
+(def gray-10 "#E3E3E3")
 (def gray-20 "#B1B2B5")
 (def gray-30 "#7B7D85")
+(def gray-40 "#64666A")
 (def info "#59B9E2")
 (def test "#fabada")
 (def white "#FFFFFF")
 (def primary "#31EFB8")
-
+(def danger "#E65244")
+(def warning "#FC8802")
diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc
index 1a586beb5..306a846b6 100644
--- a/common/src/app/common/pages/common.cljc
+++ b/common/src/app/common/pages/common.cljc
@@ -65,7 +65,8 @@
    :masked-group?         :mask-group
    :constraints-h         :constraints-group
    :constraints-v         :constraints-group
-   :fixed-scroll          :constraints-group})
+   :fixed-scroll          :constraints-group
+   :exports               :exports-group})
 
 ;; Attributes that may directly be edited by the user with forms
 (def editable-attrs
@@ -99,7 +100,9 @@
             :stroke-opacity
             :stroke-color-gradient
             :stroke-cap-start
-            :stroke-cap-end}
+            :stroke-cap-end
+
+            :exports}
 
   :group #{:proportion-lock
            :width :height
@@ -120,7 +123,9 @@
 
            :shadow
 
-           :blur}
+           :blur
+
+           :exports}
 
    :rect #{:proportion-lock
            :width :height
@@ -147,7 +152,7 @@
            :fill-color-ref-id
            :fill-color-ref-file
            :fill-color-gradient
-           
+
            :strokes
            :stroke-style
            :stroke-alignment
@@ -162,7 +167,9 @@
 
            :shadow
 
-           :blur}
+           :blur
+
+           :exports}
 
    :circle #{:proportion-lock
              :width :height
@@ -202,7 +209,9 @@
 
              :shadow
 
-             :blur}
+             :blur
+
+             :exports}
 
   :path #{:proportion-lock
           :width :height
@@ -242,7 +251,9 @@
 
           :shadow
 
-          :blur}
+          :blur
+
+          :exports}
 
   :text #{:proportion-lock
           :width :height
@@ -305,7 +316,9 @@
 
           :text-transform
 
-          :grow-type}
+          :grow-type
+
+          :exports}
 
   :image #{:proportion-lock
            :width :height
@@ -328,7 +341,9 @@
 
            :shadow
 
-           :blur}
+           :blur
+
+           :exports}
 
   :svg-raw #{:proportion-lock
              :width :height
@@ -370,7 +385,9 @@
 
              :shadow
 
-             :blur}
+             :blur
+
+             :exports}
 
   :bool #{:proportion-lock
           :width :height
@@ -410,5 +427,7 @@
 
           :shadow
 
-          :blur}})
+          :blur
+
+          :exports}})
 
diff --git a/frontend/resources/images/export-no-shapes.png b/frontend/resources/images/export-no-shapes.png
new file mode 100644
index 000000000..3e8a143c3
Binary files /dev/null and b/frontend/resources/images/export-no-shapes.png differ
diff --git a/frontend/resources/images/icons/checkbox-intermediate.svg b/frontend/resources/images/icons/checkbox-intermediate.svg
new file mode 100644
index 000000000..26502626c
--- /dev/null
+++ b/frontend/resources/images/icons/checkbox-intermediate.svg
@@ -0,0 +1 @@
+<svg id="screenshot" viewBox="10064.99280029184 896.9999999999992 40.00000000000364 40.000000000000796" width="40" height="40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="-webkit-print-color-adjust: exact;"><g id="shape-3042ec00-9b94-11ec-b905-cb847d55c7a9"><path d="M10064.99280029184,897.0000000000006L10064.99280029184,937L10104.992800291844,936.9999999999995L10104.992800291842,896.9999999999992L10064.99280029184,897.0000000000006ZL10064.99280029184,897.0000000000006ZM10067.920800291844,899.9319999999997L10102.064800291844,899.9319999999999L10102.064800291844,934.0679999999994L10067.920800291844,934.068L10067.920800291844,899.9319999999997ZL10067.920800291844,899.9319999999997ZM10073,915L10097,915L10097,919L10073,919L10073,915Z"/></g></svg>
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index 44ebe2550..222dea85c 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -96,7 +96,6 @@
     height: 30px;
     justify-content: center;
     margin-right: 16px;
-    width: 30px;
 
     svg {
       transform: rotate(45deg);
@@ -140,7 +139,7 @@
   .modal-footer {
     display: flex;
     height: 63px;
-    padding: 0px 16px;
+    padding: 0px 18px;
     border-top: 1px solid $color-gray-10;
 
     .action-buttons {
@@ -235,12 +234,17 @@
 }
 
 .import-dialog,
-.export-dialog {
+.export-dialog,
+.export-shapes-dialog {
   background-color: $color-white;
   border: 1px solid $color-gray-20;
   width: 30rem;
   min-height: 14rem;
 
+  &.no-shapes {
+    width: 39rem;
+  }
+
   p {
     font-size: $fs14;
     color: $color-black;
@@ -259,9 +263,9 @@
     border: 1px solid $color-gray-20;
     background: $color-white;
     border-radius: 3px;
-    padding: 0.3rem 1.25rem;
+    padding: 0.5rem 2.25rem;
     cursor: pointer;
-    margin-right: 8px;
+    margin-right: 18px;
 
     &:hover {
       background: $color-gray-20;
@@ -274,7 +278,7 @@
     border: 1px solid $color-primary;
     color: $color-black;
     cursor: pointer;
-    padding: 0.3rem 1.25rem;
+    padding: 0.5rem 2.25rem;
 
     &[disabled] {
       border: 1px solid #e3e3e3;
@@ -295,7 +299,7 @@
     padding-left: 2rem;
 
     h2 {
-      font-size: $fs14;
+      font-size: $fs18;
     }
   }
 
@@ -303,11 +307,6 @@
     padding: 1rem;
   }
 
-  svg {
-    max-width: 18px;
-    max-height: 18px;
-  }
-
   .file-entry {
     margin: 0.75rem 1rem;
     user-select: none;
@@ -515,6 +514,38 @@
     &.selected {
       border: 1px solid $color-primary;
     }
+
+    &.table-row {
+      align-items: center;
+      display: flex;
+      flex-wrap: wrap;
+      height: 45px;
+      justify-content: space-between;
+      padding: 0px 0px;
+      width: 100%;
+    }
+
+    .table-field {
+      flex-grow: 0;
+      padding: 0px 4px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      width: 50px;
+
+      &.check {
+        width: 30px;
+      }
+
+      &.scale {
+        flex-grow: 1;
+        width: 15%;
+      }
+
+      &.name {
+        flex-grow: 1;
+        width: 40%;
+      }
+    }
   }
 
   .option-container {
@@ -1306,3 +1337,216 @@
     }
   }
 }
+
+// Export shapes
+
+.export-progress-modal-overlay {
+  display: flex;
+  justify-content: center;
+  position: fixed;
+  right: 1rem;
+  top: 3rem;
+  padding: 16px 18px;
+  background-color: $color-white;
+  border: 1px solid $color-gray-20;
+  border-radius: 3px;
+  z-index: 1000;
+
+  &.transparent {
+    background-color: rgba($color-white, 0);
+  }
+
+  .export-progress-modal-container {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    height: 100%;
+    width: 100%;
+
+    .progress-bar {
+      margin-top: 1rem;
+    }
+
+    .export-progress-modal-header {
+      align-items: center;
+      display: flex;
+      justify-content: stretch;
+      margin-bottom: 7px;
+
+      .modal-close-button {
+        display: flex;
+        justify-content: center;
+        background-color: transparent;
+        border: none;
+        cursor: pointer;
+        padding: 2px 0;
+
+        svg {
+          height: 18px;
+          width: 18px;
+          transform: rotate(45deg);
+        }
+      }
+
+      .export-progress-modal-title {
+        padding: 0;
+        margin: 0;
+        color: $color-black;
+        flex-grow: 1;
+        font-size: $fs16;
+      }
+
+      .progress {
+        color: $color-gray-30;
+        font-size: $fs16;
+        margin-bottom: 0;
+        padding-right: 16px;
+        text-align: right;
+      }
+
+      .retry {
+        font-size: $fs12;
+        margin-right: 16px;
+        padding: 4px 12px;
+      }
+    }
+  }
+}
+
+.export-shapes-dialog {
+  .modal-content {
+    padding: 0;
+  }
+
+  .body {
+    overflow-y: auto;
+    margin: 0.5rem 0.5rem 0.5rem 0;
+  }
+
+  .field {
+    flex-grow: 0;
+    margin: 10px 0;
+    padding: 0px 4px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 50px;
+
+    &.image {
+      align-items: center;
+      border: 1px solid $color-gray-10;
+      border-radius: 3px;
+      display: flex;
+      justify-content: center;
+      height: 32px;
+      width: 32px;
+
+      svg {
+        height: 20px;
+        width: 24px;
+      }
+    }
+
+    &.check {
+      cursor: pointer;
+      height: 18px;
+      padding: 0;
+      width: 30px;
+      svg {
+        fill: $color-white;
+        max-width: 18px;
+        max-height: 18px;
+      }
+
+      & .checked {
+        svg {
+          background-color: $color-primary;
+        }
+      }
+
+      & .unchecked {
+        svg {
+          background-color: $color-gray-10;
+        }
+      }
+    }
+
+    &.title {
+      flex-grow: 1;
+      font-size: $fs12;
+      color: $color-black;
+    }
+
+    &.name {
+      flex-grow: 1;
+      font-size: $fs16;
+      color: $color-black;
+      width: 45%;
+      white-space: nowrap;
+    }
+
+    &.scale {
+      width: 25%;
+    }
+
+    &.scale,
+    &.extension {
+      color: $color-gray-30;
+      font-size: $fs12;
+    }
+  }
+
+  .header {
+    align-items: center;
+    border-bottom: 1px solid $color-gray-10;
+    display: flex;
+    flex-wrap: wrap;
+    height: 32px;
+    justify-content: space-between;
+    padding: 0.5rem 2rem;
+    margin: 0;
+    width: 100%;
+
+    .field {
+      margin: 0;
+    }
+  }
+
+  .row {
+    align-items: center;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    height: 3rem;
+    margin: 0 0.5rem 0 2rem;
+    width: calc(100% - 2.5rem);
+
+    &:not(:first-child) {
+      border-top: 1px solid $color-gray-10;
+    }
+  }
+
+  .modal-footer {
+    padding: 18px;
+  }
+
+  .no-selection {
+    padding: 2rem 1rem 2rem 2rem;
+
+    img {
+      color: $color-primary-dark;
+      float: right;
+      margin-left: 4rem;
+      width: 176px;
+    }
+
+    a {
+      font-size: $fs12;
+    }
+
+    p {
+      color: $color-gray-40;
+      font-size: $fs16;
+      padding: 1rem 0 0 0;
+    }
+  }
+}
diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss
index 3a5f1af69..a657af70e 100644
--- a/frontend/resources/styles/main/partials/workspace-header.scss
+++ b/frontend/resources/styles/main/partials/workspace-header.scss
@@ -298,8 +298,14 @@
   }
 }
 
+.export-progress-widget {
+  cursor: pointer;
+  padding-top: 6px;
+}
+
 .persistence-status-widget {
   display: flex;
+  margin-left: 0px;
   margin-right: 10px;
   /* border: 1px solid red; */
   width: 150px;
diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs
index 9fb795d9c..4c8a40355 100644
--- a/frontend/src/app/main.cljs
+++ b/frontend/src/app/main.cljs
@@ -11,6 +11,7 @@
    [app.config :as cf]
    [app.main.data.events :as ev]
    [app.main.data.users :as du]
+   [app.main.data.websocket :as ws]
    [app.main.errors]
    [app.main.sentry :as sentry]
    [app.main.store :as st]
@@ -58,7 +59,15 @@
        (->> stream
             (rx/filter du/profile-fetched?)
             (rx/take 1)
-            (rx/map #(rt/init-routes)))))))
+            (rx/map #(rt/init-routes)))
+
+       (->> stream
+            (rx/filter du/profile-fetched?)
+            (rx/map deref)
+            (rx/filter du/is-authenticated?)
+            (rx/take 1)
+            (rx/map #(ws/initialize)))))))
+
 
 (def essential-only?
   (let [href (.-href ^js glob/location)]
diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs
new file mode 100644
index 000000000..380f098f2
--- /dev/null
+++ b/frontend/src/app/main/data/exports.cljs
@@ -0,0 +1,225 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.data.exports
+  (:require
+   [app.main.data.modal :as modal]
+   [app.main.data.workspace.persistence :as dwp]
+   [app.main.data.workspace.state-helpers :as wsh]
+   [app.main.repo :as rp]
+   [app.main.store :as st]
+   [app.util.dom :as dom]
+   [app.util.time :as dt]
+   [app.util.websocket :as ws]
+   [beicon.core :as rx]
+   [potok.core :as ptk]))
+
+(def default-timeout 5000)
+
+(defn toggle-detail-visibililty
+  []
+  (ptk/reify ::toggle-detail-visibililty
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:export :detail-visible] not))))
+
+(defn toggle-widget-visibililty
+  []
+  (ptk/reify ::toggle-widget-visibility
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:export :widget-visible] not))))
+
+(defn clear-export-state
+  [id]
+  (ptk/reify ::clear-export-state
+    ptk/UpdateEvent
+    (update [_ state]
+      ;; only clear if the existing export is the same
+      (let [existing-id (-> state :export :id)]
+        (if (and (some? existing-id)
+                 (not= id existing-id))
+          state
+          (dissoc state :export))))))
+
+(defn show-workspace-export-dialog
+  ([] (show-workspace-export-dialog nil))
+  ([{:keys [selected]}]
+   (ptk/reify ::show-workspace-export-dialog
+     ptk/WatchEvent
+     (watch [_ state _]
+       (let [file-id  (:current-file-id state)
+             page-id  (:current-page-id state)
+
+             filename (-> (wsh/lookup-page state page-id) :name)
+             selected (or selected (wsh/lookup-selected state page-id {}))
+
+             shapes   (if (seq selected)
+                        (wsh/lookup-shapes state selected)
+                        (wsh/filter-shapes state #(pos? (count (:exports %)))))
+
+             exports  (for [shape  shapes
+                            export (:exports shape)]
+                        (-> export
+                            (assoc :enabled true)
+                            (assoc :page-id page-id)
+                            (assoc :file-id file-id)
+                            (assoc :object-id (:id shape))
+                            (assoc :shape (dissoc shape :exports))
+                            (assoc :name (:name shape))))]
+
+         (rx/of (modal/show :export-shapes
+                            {:exports (vec exports)
+                             :filename filename})))))))
+
+(defn show-viewer-export-dialog
+  [{:keys [shapes filename page-id file-id exports]}]
+  (ptk/reify ::show-viewer-export-dialog
+    ptk/WatchEvent
+    (watch [_ _ _]
+      (let [exports (for [shape shapes
+                          export exports]
+                      (-> export
+                          (assoc :enabled true)
+                          (assoc :page-id page-id)
+                          (assoc :file-id file-id)
+                          (assoc :object-id (:id shape))
+                          (assoc :shape (dissoc shape :exports))
+                          (assoc :name (:name shape))))]
+        (rx/of (modal/show :export-shapes {:exports (vec exports)
+                                           :filename filename}))))))
+
+(defn- initialize-export-status
+  [exports filename resource-id]
+  (ptk/reify ::initialize-export-status
+    ptk/UpdateEvent
+    (update [_ state]
+      (assoc state :export {:in-progress true
+                            :resource-id resource-id
+                            :healthy? true
+                            :error false
+                            :progress 0
+                            :widget-visible true
+                            :detail-visible true
+                            :exports exports
+                            :filename filename
+                            :last-update (dt/now)}))))
+
+(defn- update-export-status
+  [{:keys [progress status resource-id name] :as data}]
+  (ptk/reify ::update-export-status
+    ptk/UpdateEvent
+    (update [_ state]
+      (let [time-diff (dt/diff (dt/now)
+                               (get-in state [:export :last-update]))
+            healthy? (< time-diff (dt/duration {:seconds 6}))]
+        (cond-> state
+          (= status "running")
+          (update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?)
+
+          (= status "error")
+          (update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?)
+
+          (= status "ended")
+          (update :export assoc :in-progress false :last-update (dt/now) :healthy? healthy?))))
+
+    ptk/WatchEvent
+    (watch [_ _ _]
+      (when (= status "ended")
+        (->> (rp/query! :download-export-resource resource-id)
+             (rx/delay 500)
+             (rx/map #(dom/trigger-download name %)))))))
+
+(defn request-simple-export
+  [{:keys [export filename]}]
+  (ptk/reify ::request-simple-export
+    ptk/UpdateEvent
+    (update [_ state]
+      (update state :export assoc :in-progress true))
+
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [profile-id (:profile-id state)
+            params     {:exports [export]
+                        :profile-id profile-id}]
+        (rx/concat
+         (rx/of ::dwp/force-persist)
+         (->> (rp/query! :export-shapes-simple params)
+              (rx/map (fn [data]
+                        (dom/trigger-download filename data)
+                        (fn [state]
+                          (dissoc state :export))))))))))
+
+(defn request-multiple-export
+  [{:keys [filename exports] :as params}]
+  (ptk/reify ::request-multiple-export
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [resource-id (volatile! nil)
+            profile-id  (:profile-id state)
+            ws-conn     (:ws-conn state)
+            params      {:exports exports
+                         :name filename
+                         :profile-id profile-id
+                         :wait false}
+
+            progress-stream
+            (->> (ws/get-rcv-stream ws-conn)
+                 (rx/filter ws/message-event?)
+                 (rx/map :payload)
+                 (rx/filter #(= :export-update (:type %)))
+                 (rx/filter #(= @resource-id (:resource-id %)))
+                 (rx/share))
+
+            stoper
+            (->> progress-stream
+                 (rx/filter #(or (= "ended" (:status %))
+                                 (= "error" (:status %)))))]
+
+        (swap! st/ongoing-tasks conj :export)
+
+        (rx/merge
+         ;; Force that all data is persisted; best effort.
+         (rx/of ::dwp/force-persist)
+
+         ;; Launch the exportation process and stores the resource id
+         ;; locally.
+         (->> (rp/query! :export-shapes-multiple params)
+              (rx/tap (fn [{:keys [id]}]
+                        (vreset! resource-id id)))
+              (rx/map (fn [{:keys [id]}]
+                        (initialize-export-status exports filename id))))
+
+         ;; We proceed to update the export state with incoming
+         ;; progress updates. We delay the stoper for give some time
+         ;; to update the status with ended or errored status before
+         ;; close the stream.
+         (->> progress-stream
+              (rx/map update-export-status)
+              (rx/take-until (rx/delay 500 stoper))
+              (rx/finalize (fn []
+                             (swap! st/ongoing-tasks disj :export))))
+
+         ;; We hide need to hide the ui elements of the export after
+         ;; some interval. We also delay a litle bit more the stopper
+         ;; for ensure that after some security time, the stream is
+         ;; completelly closed.
+         (->> progress-stream
+              (rx/filter #(= "ended" (:status %)))
+              (rx/take 1)
+              (rx/delay default-timeout)
+              (rx/map #(clear-export-state @resource-id))
+              (rx/take-until (rx/delay 6000 stoper))))))))
+
+
+(defn retry-last-export
+  []
+  (ptk/reify ::retry-last-export
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [{:keys [exports filename]} (:export state)]
+        (rx/of (request-multiple-export {:exports exports :filename filename}))))))
+
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index cda89444a..a13b37116 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -13,6 +13,7 @@
    [app.config :as cf]
    [app.main.data.events :as ev]
    [app.main.data.media :as di]
+   [app.main.data.websocket :as ws]
    [app.main.repo :as rp]
    [app.util.i18n :as i18n]
    [app.util.router :as rt]
@@ -167,7 +168,8 @@
         (when (is-authenticated? profile)
           (->> (rx/of (profile-fetched profile)
                       (fetch-teams)
-                      (get-redirect-event))
+                      (get-redirect-event)
+                      (ws/initialize))
                (rx/observe-on :async)))))))
 
 (s/def ::invitation-token ::us/not-empty-string)
@@ -268,10 +270,12 @@
 
      ptk/WatchEvent
      (watch [_ _ _]
-       ;; NOTE: We need the `effect` of the current event to be
-       ;; executed before the redirect.
-       (->> (rx/of (rt/nav :auth-login))
-            (rx/observe-on :async)))
+       (rx/merge
+        ;; NOTE: We need the `effect` of the current event to be
+        ;; executed before the redirect.
+        (->> (rx/of (rt/nav :auth-login))
+             (rx/observe-on :async))
+        (rx/of (ws/finalize))))
 
      ptk/EffectEvent
      (effect [_ _ _]
diff --git a/frontend/src/app/main/data/websocket.cljs b/frontend/src/app/main/data/websocket.cljs
new file mode 100644
index 000000000..1fbb26770
--- /dev/null
+++ b/frontend/src/app/main/data/websocket.cljs
@@ -0,0 +1,70 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.data.websocket
+  (:require
+   [app.common.data.macros :as dm]
+   [app.common.uri :as u]
+   [app.config :as cf]
+   [app.util.websocket :as ws]
+   [beicon.core :as rx]
+   [potok.core :as ptk]))
+
+(dm/export ws/send!)
+
+(defn- prepare-uri
+  [params]
+  (let [base (-> (u/join cf/public-uri "ws/notifications")
+                 (assoc :query (u/map->query-string params)))]
+    (cond-> base
+      (= "https" (:scheme base))
+      (assoc :scheme "wss")
+
+      (= "http" (:scheme base))
+      (assoc :scheme "ws"))))
+
+(defn send
+  [message]
+  (ptk/reify ::send-message
+    ptk/EffectEvent
+    (effect [_ state _]
+      (let [ws-conn (:ws-conn state)]
+        (ws/send! ws-conn message)))))
+
+(defn initialize
+  []
+  (ptk/reify ::initialize
+    ptk/UpdateEvent
+    (update [_ state]
+      (let [sid (:session-id state)
+            uri (prepare-uri {:session-id sid})]
+        (assoc state :ws-conn (ws/create uri))))
+
+    ptk/WatchEvent
+    (watch [_ state stream]
+      (let [ws-conn (:ws-conn state)
+            stoper  (rx/merge
+                     (rx/filter (ptk/type? ::finalize) stream)
+                     (rx/filter (ptk/type? ::initialize) stream))]
+
+        (->> (rx/merge
+              (->> (ws/get-rcv-stream ws-conn)
+                   (rx/filter ws/message-event?)
+                   (rx/map :payload)
+                   (rx/map #(ptk/data-event ::message %)))
+              (->> (ws/get-rcv-stream ws-conn)
+                   (rx/filter ws/opened-event?)
+                   (rx/map (fn [_] (ptk/data-event ::opened {})))))
+             (rx/take-until stoper))))))
+
+;; --- Finalize Websocket
+
+(defn finalize
+  []
+  (ptk/reify ::finalize
+    ptk/EffectEvent
+    (effect [_ state _]
+      (some-> (:ws-conn state) ws/close!))))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index 0300d3bf0..8f5f823eb 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -116,15 +116,16 @@
             (rx/take 1)
             (rx/map deref)
             (rx/mapcat (fn [bundle]
-                         (rx/merge
-                          (rx/of (dwn/initialize file-id)
-                                 (dwp/initialize-file-persistence file-id)
-                                 (dwc/initialize-indices bundle))
+                         (let [team-id (-> bundle :project :team-id)]
+                           (rx/merge
+                            (rx/of (dwn/initialize team-id file-id)
+                                   (dwp/initialize-file-persistence file-id)
+                                   (dwc/initialize-indices bundle))
 
-                          (->> stream
-                               (rx/filter #(= ::dwc/index-initialized %))
-                               (rx/take 1)
-                               (rx/map #(file-initialized bundle)))))))))
+                            (->> stream
+                                 (rx/filter #(= ::dwc/index-initialized %))
+                                 (rx/take 1)
+                                 (rx/map #(file-initialized bundle))))))))))
 
     ptk/EffectEvent
     (effect [_ _ _]
@@ -982,7 +983,7 @@
             pages (get-in state [:workspace-data
                                  :pages-index])
             file-thumbnails (->> pages
-                     (mapcat #(extract-file-thumbnails-from-page state selected %)))]        
+                     (mapcat #(extract-file-thumbnails-from-page state selected %)))]
         (rx/concat
          (rx/from
           (for [ft file-thumbnails]
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index f104e8874..e457b6a3a 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -7,18 +7,15 @@
 (ns app.main.data.workspace.notifications
   (:require
    [app.common.data :as d]
-   [app.common.geom.point :as gpt]
    [app.common.spec :as us]
    [app.common.spec.change :as spec.change]
-   [app.common.transit :as t]
-   [app.common.uri :as u]
-   [app.config :as cf]
+   [app.common.uuid :as uuid]
+   [app.main.data.websocket :as dws]
    [app.main.data.workspace.changes :as dch]
    [app.main.data.workspace.libraries :as dwl]
    [app.main.data.workspace.persistence :as dwp]
    [app.main.streams :as ms]
    [app.util.time :as dt]
-   [app.util.websockets :as ws]
    [beicon.core :as rx]
    [cljs.spec.alpha :as s]
    [clojure.set :as set]
@@ -30,103 +27,91 @@
 (declare handle-file-change)
 (declare handle-library-change)
 (declare handle-pointer-send)
-(declare send-keepalive)
-
-(s/def ::type keyword?)
-(s/def ::message
-  (s/keys :req-un [::type]))
-
-(defn prepare-uri
-  [params]
-  (let [base (-> (u/join cf/public-uri "ws/notifications")
-                 (assoc :query (u/map->query-string params)))]
-    (cond-> base
-      (= "https" (:scheme base))
-      (assoc :scheme "wss")
-
-      (= "http" (:scheme base))
-      (assoc :scheme "ws"))))
+(declare handle-export-update)
 
 (defn initialize
-  [file-id]
+  [team-id file-id]
   (ptk/reify ::initialize
-    ptk/UpdateEvent
-    (update [_ state]
-      (let [sid (:session-id state)
-            uri (prepare-uri {:file-id file-id :session-id sid})]
-        (assoc-in state [:ws file-id] (ws/open uri))))
-
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [wsession (get-in state [:ws file-id])
-            stoper   (->> stream
-                          (rx/filter (ptk/type? ::finalize)))
-            interval (* 1000 60)]
-        (->> (rx/merge
-              ;; Each 60 seconds send a keepalive message for maintain
-              ;; this socket open.
-              (->> (rx/timer interval interval)
-                   (rx/map #(send-keepalive file-id)))
+      (let [subs-id (uuid/next)
+            stoper  (rx/filter (ptk/type? ::finalize) stream)
 
-              ;; Process all incoming messages.
-              (->> (ws/-stream wsession)
-                   (rx/filter ws/message?)
-                   (rx/map (comp t/decode-str :payload))
-                   (rx/filter #(s/valid? ::message %))
-                   (rx/map process-message))
+            initmsg [{:type :subscribe-file
+                      :subs-id subs-id
+                      :file-id file-id}
+                     {:type :subscribe-team
+                      :team-id team-id}]
 
-              (rx/of (handle-presence {:type :connect
-                                       :session-id (:session-id state)
-                                       :profile-id (:profile-id state)}))
+            endmsg  {:type :unsubscribe-file
+                     :subs-id subs-id}
 
-              ;; Send back to backend all pointer messages.
-              (->> stream
-                   (rx/filter ms/pointer-event?)
-                   (rx/sample 50)
-                   (rx/map #(handle-pointer-send file-id (:pt %)))))
-             (rx/take-until stoper))))))
+            stream  (->> (rx/merge
+                          ;; Send the subscription message
+                          (->> (rx/from initmsg)
+                               (rx/map dws/send))
+
+                          ;; Subscribe to notifications of the subscription
+                          (->> stream
+                               (rx/filter (ptk/type? ::dws/message))
+                               (rx/map deref)
+                               (rx/map process-message)
+                               (rx/filter #(= subs-id (:subs-id %))))
+
+                          ;; On reconnect, send again the subscription messages
+                          (->> stream
+                               (rx/filter (ptk/type? ::dws/opened))
+                               (rx/mapcat #(->> (rx/from initmsg)
+                                                (rx/map dws/send))))
+
+                          ;; Emit presence event for current user;
+                          ;; this is because websocket server don't
+                          ;; emits this for the same user.
+                          (rx/of (handle-presence {:type :connect
+                                                   :session-id (:session-id state)
+                                                   :profile-id (:profile-id state)}))
+
+                          ;; Emit to all other connected users the current pointer
+                          ;; position changes.
+                          (->> stream
+                               (rx/filter ms/pointer-event?)
+                               (rx/sample 50)
+                               (rx/map #(handle-pointer-send subs-id file-id (:pt %)))))
+
+                         (rx/take-until stoper))]
+
+        (rx/concat stream (rx/of (dws/send endmsg)))))))
 
 (defn- process-message
   [{:keys [type] :as msg}]
   (case type
-    :connect        (handle-presence msg)
+    :join-file      (handle-presence msg)
+    :leave-file     (handle-presence msg)
     :presence       (handle-presence msg)
     :disconnect     (handle-presence msg)
     :pointer-update (handle-pointer-update msg)
     :file-change    (handle-file-change msg)
     :library-change (handle-library-change msg)
-    ::unknown))
-
-(defn- send-keepalive
-  [file-id]
-  (ptk/reify ::send-keepalive
-    ptk/EffectEvent
-    (effect [_ state _]
-      (when-let [ws (get-in state [:ws file-id])]
-        (ws/send! ws {:type :keepalive})))))
+    nil))
 
 (defn- handle-pointer-send
-  [file-id point]
+  [subs-id file-id point]
   (ptk/reify ::handle-pointer-send
-    ptk/EffectEvent
-    (effect [_ state _]
-      (let [ws (get-in state [:ws file-id])
-            pid (:current-page-id state)
-            msg {:type :pointer-update
-                 :page-id pid
-                 :x (:x point)
-                 :y (:y point)}]
-        (ws/send! ws msg)))))
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [page-id (:current-page-id state)
+            message {:type :pointer-update
+                     :subs-id subs-id
+                     :file-id file-id
+                     :page-id page-id
+                     :position point}]
+        (rx/of (dws/send message))))))
 
 ;; --- Finalize Websocket
 
 (defn finalize
-  [file-id]
-  (ptk/reify ::finalize
-    ptk/EffectEvent
-    (effect [_ state _]
-      (when-let [ws (get-in state [:ws file-id])]
-        (ws/-close ws)))))
+  [_]
+  (ptk/reify ::finalize))
 
 ;; --- Handle: Presence
 
@@ -165,7 +150,8 @@
                 (assoc :profile-id profile-id)
                 (assoc :updated-at (dt/now))
                 (update :color update-color presence)
-                (assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"] (update-color (:color presence) presence))
+                (assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"]
+                                                  (update-color (:color presence) presence))
                                      "#000"
                                      "#fff"))))
 
@@ -179,20 +165,19 @@
     (ptk/reify ::handle-presence
       ptk/UpdateEvent
       (update [_ state]
-        ;; (let [profiles (:users state)]
-        (if (= :disconnect type)
+        (if (or (= :disconnect type) (= :leave-file type))
           (update state :workspace-presence dissoc session-id)
           (update state :workspace-presence update-presence))))))
 
 (defn handle-pointer-update
-  [{:keys [page-id session-id x y] :as msg}]
+  [{:keys [page-id session-id position] :as msg}]
   (ptk/reify ::handle-pointer-update
     ptk/UpdateEvent
     (update [_ state]
       (update-in state [:workspace-presence session-id]
                  (fn [session]
                    (assoc session
-                          :point (gpt/point x y)
+                          :point position
                           :updated-at (dt/now)
                           :page-id page-id))))))
 
@@ -241,4 +226,3 @@
       (when (contains? (:workspace-libraries state) file-id)
         (rx/of (dwl/ext-library-changed file-id modified-at revn changes)
                (dwl/notify-sync-file file-id))))))
-
diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs
index 82f856c0a..9a6793b86 100644
--- a/frontend/src/app/main/data/workspace/persistence.cljs
+++ b/frontend/src/app/main/data/workspace/persistence.cljs
@@ -30,7 +30,6 @@
    [app.main.store :as st]
    [app.util.http :as http]
    [app.util.i18n :as i18n :refer [tr]]
-   [app.util.object :as obj]
    [app.util.time :as dt]
    [app.util.uri :as uu]
    [beicon.core :as rx]
@@ -71,7 +70,7 @@
             on-dirty
             (fn []
               ;; Enable reload stoper
-              (obj/set! js/window "onbeforeunload" (constantly false))
+              (swap! st/ongoing-tasks conj :workspace-change)
               (st/emit! (update-persistence-status {:status :pending})))
 
             on-saving
@@ -81,7 +80,7 @@
             on-saved
             (fn []
               ;; Disable reload stoper
-              (obj/set! js/window "onbeforeunload" nil)
+              (swap! st/ongoing-tasks disj :workspace-change)
               (st/emit! (update-persistence-status {:status :saved})))]
         (->> (rx/merge
               (->> stream
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index b1b71a820..e02bdce16 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -7,6 +7,7 @@
 (ns app.main.data.workspace.shortcuts
   (:require
    [app.main.data.events :as ev]
+   [app.main.data.exports :as de]
    [app.main.data.shortcuts :as ds]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.colors :as mdc]
@@ -61,9 +62,14 @@
                          :command (ds/c-mod "shift+r")
                          :fn #(st/emit! (toggle-layout-flag :rules))}
 
-   :select-all          {:tooltip (ds/meta "A")
-                         :command (ds/c-mod "a")
-                         :fn #(st/emit! (dw/select-all))}
+   :export-shapes     {:tooltip (ds/meta-shift "E")
+                       :command (ds/c-mod "shift+e")
+                       :fn #(st/emit!
+                             (de/show-workspace-export-dialog))}
+
+   :select-all        {:tooltip (ds/meta "A")
+                       :command (ds/c-mod "a")
+                       :fn #(st/emit! (dw/select-all))}
 
    :toggle-grid         {:tooltip (ds/meta "'")
                          :command (ds/c-mod "'")
@@ -329,7 +335,7 @@
    :align-vcenter        {:tooltip (ds/alt "V")
                           :command "alt+v"
                           :fn #(st/emit! (dw/align-objects :vcenter))}
-  
+
    :align-bottom         {:tooltip (ds/alt "S")
                           :command "alt+s"
                           :fn #(st/emit! (dw/align-objects :vbottom))}
@@ -365,7 +371,7 @@
    :toggle-focus-mode    {:command "f"
                           :tooltip "F"
                           :fn #(st/emit! (dw/toggle-focus-mode))}
-   
+
    :thumbnail-set {:tooltip (ds/shift "T")
                    :command "shift+t"
                    :fn #(st/emit! (dw/toggle-file-thumbnail-selected))}
diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs
index bade10256..049c8c704 100644
--- a/frontend/src/app/main/data/workspace/state_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/state_helpers.cljs
@@ -7,6 +7,7 @@
 (ns app.main.data.workspace.state-helpers
   (:require
    [app.common.data :as d]
+   [app.common.data.macros :as dm]
    [app.common.pages.helpers :as cph]))
 
 (defn lookup-page
@@ -35,14 +36,16 @@
   ([state]
    (get-in state [:workspace-data :components])))
 
+;; TODO: improve performance of this
+
 (defn lookup-selected
   ([state]
    (lookup-selected state nil))
-
-  ([state {:keys [omit-blocked?]
-           :or   {omit-blocked? false}}]
-   (let [objects (lookup-page-objects state)
-         selected (->> (get-in state [:workspace-local :selected])
+  ([state options]
+   (lookup-selected state (:current-page-id state) options))
+  ([state page-id {:keys [omit-blocked?] :or {omit-blocked? false}}]
+   (let [objects  (lookup-page-objects state page-id)
+         selected (->> (dm/get-in state [:workspace-local :selected])
                        (cph/clean-loops objects))
          selectable? (fn [id]
                        (and (contains? objects id)
@@ -51,3 +54,17 @@
      (into (d/ordered-set)
            (filter selectable?)
            selected))))
+
+(defn lookup-shapes
+  ([state ids]
+   (lookup-shapes state (:current-page-id state) ids))
+  ([state page-id ids]
+   (let [objects (lookup-page-objects state page-id)]
+     (into [] (keep (d/getf objects)) ids))))
+
+(defn filter-shapes
+  ([state filter-fn]
+   (filter-shapes state (:current-page-id state) filter-fn))
+  ([state page-id filter-fn]
+   (let [objects (lookup-page-objects state page-id)]
+     (into [] (filter filter-fn) (vals objects)))))
diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs
index d6587b47c..12daf2712 100644
--- a/frontend/src/app/main/errors.cljs
+++ b/frontend/src/app/main/errors.cljs
@@ -12,7 +12,6 @@
    [app.config :as cf]
    [app.main.data.messages :as msg]
    [app.main.data.users :as du]
-   [app.main.sentry :as sentry]
    [app.main.store :as st]
    [app.util.i18n :refer [tr]]
    [app.util.router :as rt]
@@ -26,7 +25,7 @@
   [error]
   (cond
     (instance? ExceptionInfo error)
-    (-> error sentry/capture-exception ex-data ptk/handle-error)
+    (-> error ex-data ptk/handle-error)
 
     (map? error)
     (ptk/handle-error error)
@@ -34,7 +33,6 @@
     :else
     (let [hint (ex-message error)
           msg  (dm/str "Internal Error: " hint)]
-      (sentry/capture-exception error)
       (ts/schedule (st/emitf (rt/assign-exception error)))
 
       (js/console.group msg)
@@ -68,7 +66,8 @@
 ;; Error that happens on an active business model validation does not
 ;; passes an validation (example: profile can't leave a team). From
 ;; the user perspective a error flash message should be visualized but
-;; user can continue operate on the application.
+;; user can continue operate on the application. Can happen in backend
+;; and frontend.
 (defmethod ptk/handle-error :validation
   [error]
   (ts/schedule
@@ -92,6 +91,7 @@
 
 
 ;; Error on parsing an SVG
+;; TODO: looks unused and deprecated
 (defmethod ptk/handle-error :svg-parser
   [_]
   (ts/schedule
@@ -100,6 +100,7 @@
               :type :error
               :timeout 3000}))))
 
+;; TODO: should be handled in the event and not as general error handler
 (defmethod ptk/handle-error :comment-error
   [_]
   (ts/schedule
@@ -160,10 +161,9 @@
 (defn on-unhandled-error
   [error]
   (if (instance? ExceptionInfo error)
-    (-> error sentry/capture-exception ex-data ptk/handle-error)
+    (-> error ex-data ptk/handle-error)
     (let [hint (ex-message error)
           msg  (dm/str "Unhandled Internal Error: " hint)]
-      (sentry/capture-exception error)
       (ts/schedule (st/emitf (rt/assign-exception error)))
       (js/console.group msg)
       (ex/ignoring (js/console.error error))
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 1528dd1cc..14a4a9259 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -42,6 +42,9 @@
 (def share-links
   (l/derived :share-links st/state))
 
+(def export
+  (l/derived :export st/state))
+
 ;; ---- Dashboard refs
 
 (def dashboard-local
@@ -98,6 +101,7 @@
 (def workspace-drawing
   (l/derived :workspace-drawing st/state))
 
+;; TODO: rename to workspace-selected (?)
 (def selected-shapes
   (l/derived wsh/lookup-selected st/state =))
 
@@ -105,6 +109,27 @@
   [id]
   (l/derived #(contains? % id) selected-shapes))
 
+(def export-in-progress?
+  (l/derived :export-in-progress? export))
+
+(def export-error?
+  (l/derived :export-error? export))
+
+(def export-progress
+  (l/derived :export-progress export))
+
+(def exports
+  (l/derived :exports export))
+
+(def export-detail-visibililty
+  (l/derived :export-detail-visibililty export))
+
+(def export-widget-visibililty
+  (l/derived :export-widget-visibililty export))
+
+(def export-health
+  (l/derived :export-health export))
+
 (def selected-zoom
   (l/derived :zoom workspace-local))
 
@@ -233,11 +258,7 @@
 
 (defn objects-by-id
   [ids]
-  (let [selector
-        (fn [state]
-          (let [objects (wsh/lookup-page-objects state)]
-            (into [] (keep (d/getf objects)) ids)))]
-    (l/derived selector st/state =)))
+  (l/derived #(wsh/lookup-shapes % ids) st/state =))
 
 (defn- set-content-modifiers [state]
   (fn [id shape]
diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs
index 9fa42387c..8574e1905 100644
--- a/frontend/src/app/main/repo.cljs
+++ b/frontend/src/app/main/repo.cljs
@@ -8,7 +8,7 @@
   (:require
    [app.common.data :as d]
    [app.common.uri :as u]
-   [app.config :as cfg]
+   [app.config :as cf]
    [app.util.http :as http]
    [beicon.core :as rx]))
 
@@ -40,7 +40,7 @@
                :status status
                :data body})))
 
-(def ^:private base-uri cfg/public-uri)
+(def ^:private base-uri cf/public-uri)
 
 (defn- send-query!
   "A simple helper for send and receive transit data on the penpot
@@ -105,23 +105,44 @@
        (rx/map http/conditional-decode-transit)
        (rx/mapcat handle-response)))
 
-(defmethod query :export
-  [_ params]
+(defn- send-export-command
+  [& {:keys [cmd params blob?]}]
   (->> (http/send! {:method :post
                     :uri (u/join base-uri "export")
-                    :body (http/transit-data params)
+                    :body (http/transit-data (assoc params :cmd cmd))
                     :credentials "include"
-                    :response-type :blob})
+                    :response-type (if blob? :blob :text)})
+       (rx/map http/conditional-decode-transit)
        (rx/mapcat handle-response)))
 
-(defmethod query :export-frames
+(defmethod query :export-shapes-simple
   [_ params]
-  (->> (http/send! {:method :post
-                    :uri (u/join base-uri "export-frames")
-                    :body (http/transit-data params)
-                    :credentials "include"
-                    :response-type :blob})
-       (rx/mapcat handle-response)))
+  (let [params (merge {:wait true} params)]
+    (->> (rx/of params)
+         (rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false))
+         (rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true)))))
+
+(defmethod query :export-shapes-multiple
+  [_ params]
+  (send-export-command :cmd :export-shapes :params params :blob? false))
+
+(defmethod query :download-export-resource
+  [_ id]
+  (send-export-command :cmd :get-resource :params {:id id} :blob? true))
+
+(defmethod query :export-frames
+  [_ exports]
+  (let [params {:uri (str base-uri)
+                :cmd :export-frames
+                :wait false
+                :exports exports}]
+    (->> (http/send! {:method :post
+                      :uri (u/join base-uri "export")
+                      :body (http/transit-data params)
+                      :credentials "include"
+                      :response-type :blob})
+         (rx/mapcat handle-response)
+         (rx/ignore))))
 
 (derive :upload-file-media-object ::multipart-upload)
 (derive :update-profile-photo ::multipart-upload)
diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs
index 35936207d..01beedb9e 100644
--- a/frontend/src/app/main/store.cljs
+++ b/frontend/src/app/main/store.cljs
@@ -7,6 +7,7 @@
 (ns app.main.store
   (:require-macros [app.main.store])
   (:require
+   [app.util.object :as obj]
    [beicon.core :as rx]
    [okulary.core :as l]
    [potok.core :as ptk]))
@@ -59,4 +60,10 @@
   [& events]
   #(apply ptk/emit! state events))
 
+(defonce ongoing-tasks (l/atom #{}))
 
+(add-watch ongoing-tasks ::ongoing-tasks
+           (fn [_ _ _ events]
+             (if (empty? events)
+               (obj/set! js/window "onbeforeunload" nil)
+               (obj/set! js/window "onbeforeunload" (constantly false)))))
diff --git a/frontend/src/app/main/ui/export.cljs b/frontend/src/app/main/ui/export.cljs
new file mode 100644
index 000000000..14f284554
--- /dev/null
+++ b/frontend/src/app/main/ui/export.cljs
@@ -0,0 +1,211 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.export
+  "Assets exportation common components."
+  (:require
+   [app.common.colors :as clr]
+   [app.common.data :as d]
+   [app.common.data.macros :as dm]
+   [app.main.data.exports :as de]
+   [app.main.data.modal :as modal]
+   [app.main.refs :as refs]
+   [app.main.store :as st]
+   [app.main.ui.icons :as i]
+   [app.main.ui.workspace.shapes :refer [shape-wrapper]]
+   [app.util.dom :as dom]
+   [app.util.i18n :as i18n :refer  [tr c]]
+   [cuerdas.core :as str]
+   [rumext.alpha :as mf]))
+
+(mf/defc export-shapes-dialog
+  {::mf/register modal/components
+   ::mf/register-as :export-shapes}
+  [{:keys [exports filename]}]
+  (let [lstate          (mf/deref refs/export)
+        in-progress?    (:in-progress lstate)
+
+        exports         (mf/use-state exports)
+
+        all-exports     (deref exports)
+        all-checked?    (every? :enabled all-exports)
+        all-unchecked?  (every? (complement :enabled) all-exports)
+
+        enabled-exports (into [] (filter :enabled) all-exports)
+
+        cancel-fn
+        (fn [event]
+          (dom/prevent-default event)
+          (st/emit! (modal/hide)))
+
+        accept-fn
+        (fn [event]
+          (dom/prevent-default event)
+          (st/emit! (modal/hide)
+                    (de/request-multiple-export {:filename filename :exports enabled-exports})))
+        on-toggle-enabled
+        (fn [index]
+          (swap! exports update-in [index :enabled] not))
+
+        change-all
+        (fn [_]
+          (swap! exports (fn [exports]
+                           (mapv #(assoc % :enabled (not all-checked?)) exports))))
+        ]
+    [:div.modal-overlay
+     [:div.modal-container.export-shapes-dialog
+      {:class (when (empty? all-exports) "no-shapes")}
+
+      [:div.modal-header
+       [:div.modal-header-title
+        [:h2 (tr "dashboard.export-shapes.title")]]
+
+       [:div.modal-close-button
+        {:on-click cancel-fn} i/close]]
+
+      [:*
+       [:div.modal-content
+        (if (> (count all-exports) 0)
+          [:*
+           [:div.header
+            [:div.field.check {:on-click change-all}
+             (cond
+               all-checked? [:span i/checkbox-checked]
+               all-unchecked? [:span i/checkbox-unchecked]
+               :else [:span i/checkbox-intermediate])]
+            [:div.field.title (tr "dashboard.export-shapes.selected"
+                                  (c (count enabled-exports))
+                                  (c (count all-exports)))]]
+
+           [:div.body
+            (for [[index {:keys [shape suffix] :as export}] (d/enumerate @exports)]
+              (let [{:keys [x y width height]} (:selrect shape)]
+                [:div.row
+                 [:div.field.check {:on-click #(on-toggle-enabled index)}
+                  (if (:enabled export)
+                    [:span.checked i/checkbox-checked]
+                    [:span.unchecked i/checkbox-unchecked])]
+
+                 [:div.field.image
+                  [:svg {:view-box (dm/str x " " y " " width " " height)
+                         :width 24
+                         :height 20
+                         :version "1.1"
+                         :xmlns "http://www.w3.org/2000/svg"
+                         :xmlnsXlink "http://www.w3.org/1999/xlink"
+                         ;; Fix Chromium bug about color of html texts
+                         ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
+                         :style {:-webkit-print-color-adjust :exact}}
+
+                   [:& shape-wrapper {:shape shape}]]]
+
+                 [:div.field.name (cond-> (:name shape) suffix (str suffix))]
+                 [:div.field.scale (dm/str (* width (:scale export)) "x"
+                                           (* height (:scale export)) "px ")]
+                 [:div.field.extension (-> export :type d/name str/upper)]]))]
+
+           [:div.modal-footer
+            [:div.action-buttons
+             [:input.cancel-button
+              {:type "button"
+               :value (tr "labels.cancel")
+               :on-click cancel-fn}]
+
+             [:input.accept-button.primary
+              {:class (dom/classnames
+                       :btn-disabled (or in-progress? all-unchecked?))
+               :disabled (or in-progress? all-unchecked?)
+               :type "button"
+               :value (if in-progress?
+                        (tr "workspace.options.exporting-object")
+                        (tr "labels.export"))
+               :on-click (when-not in-progress? accept-fn)}]]]]
+
+          [:div.no-selection
+           [:img {:src "images/export-no-shapes.png" :border "0"}]
+           [:p (tr "dashboard.export-shapes.no-elements")]
+           [:p (tr "dashboard.export-shapes.how-to")]
+           [:p [:a {:target "_blank"
+                    :href "https://help.penpot.app/user-guide/exporting/ "}
+                (tr "dashboard.export-shapes.how-to-link")]]])]]]]))
+
+(mf/defc export-progress-widget
+  {::mf/wrap [mf/memo]}
+  []
+  (let [state           (mf/deref refs/export)
+        error?          (:error state)
+        healthy?        (:healthy? state)
+        detail-visible? (:detail-visible state)
+        widget-visible? (:widget-visible state)
+        progress        (:progress state)
+        exports         (:exports state)
+        total           (count exports)
+        circ            (* 2 Math/PI 12)
+        pct             (- circ (* circ (/ progress total)))
+
+        pwidth (if error?
+                 280
+                 (/ (* progress 280) total))
+        color  (cond
+                 error?         clr/danger
+                 healthy?       clr/primary
+                 (not healthy?) clr/warning)
+        title  (cond
+                 error?          (tr "workspace.options.exporting-object-error")
+                 healthy?        (tr "workspace.options.exporting-object")
+                 (not healthy?)  (tr "workspace.options.exporting-object-slow"))
+
+        retry-last-export
+        (mf/use-fn #(st/emit! (de/retry-last-export)))
+
+        toggle-detail-visibility
+        (mf/use-fn #(st/emit! (de/toggle-detail-visibililty)))]
+
+    [:*
+     (when widget-visible?
+       [:div.export-progress-widget {:on-click toggle-detail-visibility}
+        [:svg {:width "32" :height "32"}
+         [:circle {:r "12"
+                   :cx "16"
+                   :cy "16"
+                   :fill "transparent"
+                   :stroke clr/gray-40
+                   :stroke-width "4"}]
+         [:circle {:r "12"
+                   :cx "16"
+                   :cy "16"
+                   :fill "transparent"
+                   :stroke color
+                   :stroke-width "4"
+                   :stroke-dasharray (dm/str circ " " circ)
+                   :stroke-dashoffset pct
+                   :transform "rotate(-90 16,16)"
+                   :style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])
+
+     (when detail-visible?
+       [:div.export-progress-modal-overlay
+        [:div.export-progress-modal-container
+         [:div.export-progress-modal-header
+          [:p.export-progress-modal-title title]
+          (if error?
+            [:button.btn-secondary.retry {:on-click retry-last-export} (tr "workspace.options.retry")]
+            [:p.progress (dm/str progress " / " total)])
+
+          [:button.modal-close-button {:on-click toggle-detail-visibility} i/close]]
+
+         [:svg.progress-bar {:height 8 :width 280}
+          [:g
+           [:path {:d "M0 0 L280 0"
+                   :stroke clr/gray-10
+                   :stroke-width 30}]
+           [:path {:d (dm/str "M0 0 L280 0")
+                   :stroke color
+                   :stroke-width 30
+                   :fill "transparent"
+                   :stroke-dasharray 280
+                   :stroke-dashoffset (- 280 pwidth)
+                   :style {:transition "stroke-dashoffset 1s ease-in-out"}}]]]]])]))
+
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 944f46aae..c10126c61 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -39,6 +39,7 @@
 (def chat (icon-xref :chat))
 (def checkbox-checked (icon-xref :checkbox-checked))
 (def checkbox-unchecked (icon-xref :checkbox-unchecked))
+(def checkbox-intermediate (icon-xref :checkbox-intermediate))
 (def circle (icon-xref :circle))
 (def close (icon-xref :close))
 (def code (icon-xref :code))
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
index 824876efd..52518b738 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
@@ -7,6 +7,7 @@
 (ns app.main.ui.viewer.handoff.attributes
   (:require
    [app.common.geom.shapes :as gsh]
+   [app.main.ui.hooks :as hooks]
    [app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
    [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
    [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
@@ -16,7 +17,6 @@
    [app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]]
    [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]
    [app.main.ui.viewer.handoff.exports :refer [exports]]
-   [app.util.i18n :as i18n]
    [rumext.alpha :as mf]))
 
 (def type->options
@@ -31,8 +31,9 @@
 
 (mf/defc attributes
   [{:keys [page-id file-id shapes frame]}]
-  (let [locale  (mf/deref i18n/locale)
-        shapes  (->> shapes (map #(gsh/translate-to-frame % frame)))
+  (let [shapes  (hooks/use-equal-memo shapes)
+        shapes  (mf/with-memo [shapes]
+                  (mapv #(gsh/translate-to-frame % frame) shapes))
         type    (if (= (count shapes) 1) (-> shapes first :type) :multiple)
         options (type->options type)]
     [:div.element-options
@@ -47,10 +48,9 @@
              :text   text-panel
              :svg    svg-panel)
         {:shapes shapes
-         :frame frame
-         :locale locale}])
-     (when-not (= :multiple type)
-       [:& exports
-        {:shape (first shapes)
-         :page-id page-id
-         :file-id file-id}])]))
+         :frame frame}])
+     [:& exports
+      {:shapes shapes
+       :type type
+       :page-id page-id
+       :file-id file-id}]]))
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs
index fc9f382ea..849190bae 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs
@@ -8,7 +8,7 @@
   (:require
    [app.main.ui.components.copy-button :refer [copy-button]]
    [app.util.code-gen :as cg]
-   [app.util.i18n :refer [t]]
+   [app.util.i18n :refer [tr]]
    [cuerdas.core :as str]
    [rumext.alpha :as mf]))
 
@@ -22,17 +22,17 @@
    {:to-prop "filter"
     :format #(str/fmt "blur(%spx)" (:value %))}))
 
-(mf/defc blur-panel [{:keys [shapes locale]}]
+(mf/defc blur-panel [{:keys [shapes]}]
   (let [shapes (->> shapes (filter has-blur?))]
     (when (seq shapes)
       [:div.attributes-block
        [:div.attributes-block-title
-        [:div.attributes-block-title-text (t locale "handoff.attributes.blur")]
+        [:div.attributes-block-title-text (tr "handoff.attributes.blur")]
         (when (= (count shapes) 1)
           [:& copy-button {:data (copy-data (first shapes))}])]
 
        (for [shape shapes]
          [:div.attributes-unit-row
-          [:div.attributes-label (t locale "handoff.attributes.blur.value")]
+          [:div.attributes-label (tr "handoff.attributes.blur.value")]
           [:div.attributes-value (-> shape :blur :value) "px"]
           [:& copy-button {:data (copy-data shape)}]])])))
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs
index 624ae2ab4..adda6a818 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs
@@ -11,7 +11,7 @@
    [app.main.ui.components.copy-button :refer [copy-button]]
    [app.util.color :as uc]
    [app.util.dom :as dom]
-   [app.util.i18n :refer [t] :as i18n]
+   [app.util.i18n :refer [tr]]
    [cuerdas.core :as str]
    [okulary.core :as l]
    [rumext.alpha :as mf]))
@@ -27,9 +27,7 @@
     #(l/derived get-library st/state)))
 
 (mf/defc color-row [{:keys [color format copy-data on-change-format]}]
-  (let [locale (mf/deref i18n/locale)
-
-        colors-library-ref (mf/use-memo
+  (let [colors-library-ref (mf/use-memo
                             (mf/deps (:file-id color))
                             (make-colors-library-ref (:file-id color)))
         colors-library (mf/deref colors-library-ref)
@@ -60,13 +58,13 @@
       (when-not (and on-change-format (:gradient color))
         [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
          [:option {:value "hex"}
-          (t locale "handoff.attributes.color.hex")]
+          (tr "handoff.attributes.color.hex")]
 
          [:option {:value "rgba"}
-          (t locale "handoff.attributes.color.rgba")]
+          (tr "handoff.attributes.color.rgba")]
 
          [:option {:value "hsla"}
-          (t locale "handoff.attributes.color.hsla")]])]
+          (tr "handoff.attributes.color.hsla")]])]
      (when copy-data
        [:& copy-button {:data copy-data}])]))
 
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs
index ccbd9abc9..fe7c7faa6 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs
@@ -9,7 +9,7 @@
    [app.common.spec.radius :as ctr]
    [app.main.ui.components.copy-button :refer [copy-button]]
    [app.util.code-gen :as cg]
-   [app.util.i18n :refer [t]]
+   [app.util.i18n :refer [tr]]
    [cuerdas.core :as str]
    [rumext.alpha :as mf]))
 
@@ -32,41 +32,41 @@
    (cg/generate-css-props shape properties params)))
 
 (mf/defc layout-block
-  [{:keys [shape locale]}]
+  [{:keys [shape]}]
   (let [selrect (:selrect shape)
         {:keys [width height x y]} selrect]
     [:*
      [:div.attributes-unit-row
-      [:div.attributes-label (t locale "handoff.attributes.layout.width")]
+      [:div.attributes-label (tr "handoff.attributes.layout.width")]
       [:div.attributes-value width "px"]
       [:& copy-button {:data (copy-data selrect :width)}]]
 
      [:div.attributes-unit-row
-      [:div.attributes-label (t locale "handoff.attributes.layout.height")]
+      [:div.attributes-label (tr "handoff.attributes.layout.height")]
       [:div.attributes-value height "px"]
       [:& copy-button {:data (copy-data selrect :height)}]]
 
      (when (not= (:x shape) 0)
        [:div.attributes-unit-row
-        [:div.attributes-label (t locale "handoff.attributes.layout.left")]
+        [:div.attributes-label (tr "handoff.attributes.layout.left")]
         [:div.attributes-value x "px"]
         [:& copy-button {:data (copy-data selrect :x)}]])
 
      (when (not= (:y shape) 0)
        [:div.attributes-unit-row
-        [:div.attributes-label (t locale "handoff.attributes.layout.top")]
+        [:div.attributes-label (tr "handoff.attributes.layout.top")]
         [:div.attributes-value y "px"]
         [:& copy-button {:data (copy-data selrect :y)}]])
 
      (when (ctr/radius-1? shape)
        [:div.attributes-unit-row
-        [:div.attributes-label (t locale "handoff.attributes.layout.radius")]
+        [:div.attributes-label (tr "handoff.attributes.layout.radius")]
         [:div.attributes-value (:rx shape 0) "px"]
         [:& copy-button {:data (copy-data shape :rx)}]])
 
      (when (ctr/radius-4? shape)
        [:div.attributes-unit-row
-        [:div.attributes-label (t locale "handoff.attributes.layout.radius")]
+        [:div.attributes-label (tr "handoff.attributes.layout.radius")]
         [:div.attributes-value
          (:r1 shape) ", "
          (:r2 shape) ", "
@@ -76,19 +76,18 @@
 
      (when (not= (:rotation shape 0) 0)
        [:div.attributes-unit-row
-        [:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
+        [:div.attributes-label (tr "handoff.attributes.layout.rotation")]
         [:div.attributes-value (:rotation shape) "deg"]
         [:& copy-button {:data (copy-data shape :rotation)}]])]))
 
 
 (mf/defc layout-panel
-  [{:keys [shapes locale]}]
+  [{:keys [shapes]}]
   [:div.attributes-block
    [:div.attributes-block-title
-    [:div.attributes-block-title-text (t locale "handoff.attributes.layout")]
+    [:div.attributes-block-title-text (tr "handoff.attributes.layout")]
     (when (= (count shapes) 1)
       [:& copy-button {:data (copy-data (first shapes))}])]
 
    (for [shape shapes]
-     [:& layout-block {:shape shape
-                       :locale locale}])])
+     [:& layout-block {:shape shape}])])
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs
index 1564d73c5..a5fea5283 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs
@@ -11,7 +11,7 @@
    [app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
    [app.util.code-gen :as cg]
    [app.util.color :as uc]
-   [app.util.i18n :refer [t]]
+   [app.util.i18n :refer [tr]]
    [cuerdas.core :as str]
    [rumext.alpha :as mf]))
 
@@ -52,7 +52,7 @@
     :format #(uc/color->background (shape->color shape))}))
 
 (mf/defc stroke-block
-  [{:keys [shape locale]}]
+  [{:keys [shape]}]
   (let [color-format (mf/use-state :hex)
         color (shape->color shape)]
     [:*
@@ -65,19 +65,19 @@
            stroke-style (if (= stroke-style :svg) :solid stroke-style)
            stroke-alignment (or stroke-alignment :center)]
        [:div.attributes-stroke-row
-        [:div.attributes-label (t locale "handoff.attributes.stroke.width")]
+        [:div.attributes-label (tr "handoff.attributes.stroke.width")]
         [:div.attributes-value (:stroke-width shape) "px"]
-        [:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
-        [:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
+        [:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (tr))]
+        [:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (tr))]
         [:& copy-button {:data (copy-stroke-data shape)}]])]))
 
 (mf/defc stroke-panel
-  [{:keys [shapes locale]}]
+  [{:keys [shapes]}]
   (let [shapes (->> shapes (filter has-stroke?))]
     (when (seq shapes)
       [:div.attributes-block
        [:div.attributes-block-title
-        [:div.attributes-block-title-text (t locale "handoff.attributes.stroke")]
+        [:div.attributes-block-title-text (tr "handoff.attributes.stroke")]
         (when (= (count shapes) 1)
           [:& copy-button {:data (copy-stroke-data (first shapes))}])]
 
@@ -85,8 +85,6 @@
          (if (seq (:strokes shape))
            (for [value (:strokes shape [])]
              [:& stroke-block {:key (str "stroke-color-" (:id shape))
-                               :shape value
-                               :locale locale}])
+                               :shape value}])
            [:& stroke-block {:key (str "stroke-color-" (:id shape))
-                             :shape shape
-                             :locale locale}]))])))
+                             :shape shape}]))])))
diff --git a/frontend/src/app/main/ui/viewer/handoff/exports.cljs b/frontend/src/app/main/ui/viewer/handoff/exports.cljs
index 930281d30..aec4715f4 100644
--- a/frontend/src/app/main/ui/viewer/handoff/exports.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/exports.cljs
@@ -7,21 +7,56 @@
 (ns app.main.ui.viewer.handoff.exports
   (:require
    [app.common.data :as d]
+   [app.main.data.exports :as de]
+   [app.main.refs :as refs]
+   [app.main.store :as st]
    [app.main.ui.icons :as i]
-   [app.main.ui.workspace.sidebar.options.menus.exports :as we]
    [app.util.dom :as dom]
-   [app.util.i18n :refer [tr]]
+   [app.util.i18n :refer [tr c]]
    [rumext.alpha :as mf]))
 
 (mf/defc exports
-  [{:keys [shape page-id file-id] :as props}]
-  (let [exports  (mf/use-state (:exports shape []))
+  {::mf/wrap [#(mf/memo % =)]}
+  [{:keys [shapes page-id file-id type] :as props}]
+  (let [exports     (mf/use-state [])
+        xstate      (mf/deref refs/export)
+        vstate      (mf/deref refs/viewer-data)
+        page        (get-in vstate [:pages page-id])
+        filename    (if (= (count shapes) 1)
+                      (let [sname   (-> shapes first :name)
+                            suffix (-> @exports first :suffix)]
+                        (cond-> sname
+                          (and (= 1 (count @exports)) (some? suffix))
+                          (str suffix)))
+                      (:name page))
 
-        [on-download loading?] (we/use-download-export shape page-id file-id @exports)
+        in-progress? (:in-progress xstate)
+
+        on-download
+        (fn [event]
+          (dom/prevent-default event)
+          (if (= :multiple type)
+            (st/emit! (de/show-viewer-export-dialog {:shapes shapes
+                                                     :exports @exports
+                                                     :filename filename
+                                                     :page-id page-id
+                                                     :file-id file-id}))
+
+            ;; In other all cases we only allowed to have a single
+            ;; shape-id because multiple shape-ids are handled
+            ;; separatelly by the export-modal.
+            (let [defaults {:page-id page-id
+                            :file-id file-id
+                            :name filename
+                            :object-id (-> shapes first :id)}
+                  exports  (mapv #(merge % defaults) @exports)]
+              (if (= 1 (count exports))
+                (st/emit! (de/request-simple-export {:export (first exports)}))
+                (st/emit! (de/request-multiple-export {:exports exports :filename filename}))))))
 
         add-export
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps shapes)
          (fn []
            (let [xspec {:type :png
                         :suffix ""
@@ -30,7 +65,7 @@
 
         delete-export
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps shapes)
          (fn [index]
            (swap! exports (fn [exports]
                             (let [[before after] (split-at index exports)]
@@ -38,7 +73,7 @@
 
         on-scale-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps shapes)
          (fn [index event]
            (let [target  (dom/get-target event)
                  value   (dom/get-value target)
@@ -47,7 +82,7 @@
 
         on-suffix-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps shapes)
          (fn [index event]
            (let [target  (dom/get-target event)
                  value   (dom/get-value target)]
@@ -55,7 +90,7 @@
 
         on-type-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps shapes)
          (fn [index event]
            (let [target  (dom/get-target event)
                  value   (dom/get-value target)
@@ -63,9 +98,12 @@
              (swap! exports assoc-in [index :type] value))))]
 
     (mf/use-effect
-     (mf/deps shape)
+     (mf/deps shapes)
      (fn []
-       (reset! exports (:exports shape []))))
+       (reset! exports (-> (mapv #(:exports % []) shapes)
+                           flatten
+                           distinct
+                           vec))))
 
     [:div.element-set.exports-options
      [:div.element-set-title
@@ -99,10 +137,10 @@
             i/minus]])
 
         [:div.btn-icon-dark.download-button
-         {:on-click (when-not loading? on-download)
-          :class (dom/classnames :btn-disabled loading?)
-          :disabled loading?}
-         (if loading?
+         {:on-click (when-not in-progress? on-download)
+          :class (dom/classnames :btn-disabled in-progress?)
+          :disabled in-progress?}
+         (if in-progress?
            (tr "workspace.options.exporting-object")
-           (tr "workspace.options.export-object"))]])]))
+           (tr "workspace.options.export-object" (c (count shapes))))]])]))
 
diff --git a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
index ac9d30d3f..55ae526da 100644
--- a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
@@ -20,8 +20,8 @@
   [{:keys [frame page file selected]}]
   (let [expanded      (mf/use-state false)
         section       (mf/use-state :info #_:code)
-
         shapes        (resolve-shapes (:objects page) selected)
+
         first-shape   (first shapes)
 
         selected-type (or (:type first-shape) :not-found)
diff --git a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs
index a93fbbdea..4445eee30 100644
--- a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs
@@ -6,6 +6,7 @@
 
 (ns app.main.ui.viewer.handoff.selection-feedback
   (:require
+   [app.common.data :as d]
    [app.common.geom.shapes :as gsh]
    [app.main.ui.measurements :refer [selection-guides size-display measurement]]
    [rumext.alpha :as mf]))
@@ -21,10 +22,8 @@
 
 (defn resolve-shapes
   [objects ids]
-  (let [resolve-shape #(get objects %)]
-    (into [] (comp (map resolve-shape)
-                   (filter some?))
-          ids)))
+  (let [resolve-shape (d/getf objects)]
+    (into [] (keep resolve-shape) ids)))
 
 ;; ------------------------------------------------
 ;; HELPERS
diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs
index b32adfbc1..257b4f8d8 100644
--- a/frontend/src/app/main/ui/viewer/header.cljs
+++ b/frontend/src/app/main/ui/viewer/header.cljs
@@ -12,12 +12,13 @@
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.dropdown :refer [dropdown]]
+   [app.main.ui.export :refer [export-progress-widget]]
    [app.main.ui.formats :as fmt]
    [app.main.ui.icons :as i]
    [app.main.ui.viewer.comments :refer [comments-menu]]
    [app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
    [app.util.dom :as dom]
-   [app.util.i18n :as i18n :refer [tr]]
+   [app.util.i18n :refer [tr]]
    [rumext.alpha :as mf]))
 
 (mf/defc zoom-widget
@@ -88,6 +89,7 @@
 
        [:div.view-options])
 
+     [:& export-progress-widget]
      [:& zoom-widget
       {:zoom zoom
        :on-increase (st/emitf dv/increase-zoom)
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index b7097bd39..221f1f2c0 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -7,9 +7,11 @@
 (ns app.main.ui.workspace.header
   (:require
    [app.common.data :as d]
+   [app.common.data.macros :as dm]
    [app.config :as cf]
    [app.main.data.events :as ev]
-   [app.main.data.messages :as dm]
+   [app.main.data.exports :as de]
+   [app.main.data.messages :as msg]
    [app.main.data.modal :as modal]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.shortcuts :as sc]
@@ -17,6 +19,7 @@
    [app.main.repo :as rp]
    [app.main.store :as st]
    [app.main.ui.components.dropdown :refer [dropdown]]
+   [app.main.ui.export :refer [export-progress-widget]]
    [app.main.ui.formats :as fmt]
    [app.main.ui.hooks.resize :as r]
    [app.main.ui.icons :as i]
@@ -30,11 +33,12 @@
    [potok.core :as ptk]
    [rumext.alpha :as mf]))
 
-;; --- Zoom Widget
 
 (def workspace-persistence-ref
   (l/derived :workspace-persistence st/state))
 
+;; --- Persistence state Widget
+
 (mf/defc persistence-state-widget
   {::mf/wrap [mf/memo]}
   []
@@ -60,6 +64,8 @@
         [:span.icon i/msg-warning]
         [:span.label (tr "workspace.header.save-error")]])]))
 
+;; --- Zoom Widget
+
 (mf/defc zoom-widget-workspace
   {::mf/wrap [mf/memo]}
   [{:keys [zoom
@@ -150,6 +156,11 @@
                              (dom/prevent-default event)
                              (reset! editing? true))
 
+        on-export-shapes
+        (mf/use-callback
+         (fn [_]
+           (st/emit! (de/show-workspace-export-dialog))))
+
         on-export-file
         (mf/use-callback
          (mf/deps file team-id)
@@ -178,21 +189,20 @@
          (mf/deps file frames)
          (fn [_]
            (when (seq frames)
-             (let [filename  (str (:name file) ".pdf")
-                   frame-ids (mapv :id frames)]
-               (st/emit! (dm/info (tr "workspace.options.exporting-object")
-                                  {:timeout nil}))
-               (->> (rp/query! :export-frames
-                               {:name     (:name file)
-                                :file-id  (:id file)
-                                :page-id   page-id
-                                :frame-ids frame-ids})
+             (let [filename (dm/str (:name file) ".pdf")
+                   xform    (comp (map :id)
+                                  (map (fn [id]
+                                         {:file-id  (:id file)
+                                          :page-id   page-id
+                                          :frame-id id})))]
+               (st/emit! (msg/info (tr "workspace.options.exporting-object") {:timeout nil}))
+               (->> (rp/query! :export-frames (into [] xform frames))
                     (rx/subs
                      (fn [body]
                        (dom/trigger-download filename body))
                      (fn [_error]
-                       (st/emit! (dm/error (tr "errors.unexpected-error"))))
-                     (st/emitf dm/hide)))))))
+                       (st/emit! (msg/error (tr "errors.unexpected-error"))))
+                     (st/emitf msg/hide)))))))
 
         on-item-hover
         (mf/use-callback
@@ -269,6 +279,9 @@
           [:span (tr "dashboard.remove-shared")]]
          [:li {:on-click on-add-shared}
           [:span (tr "dashboard.add-shared")]])
+       [:li.export-file {:on-click on-export-shapes}
+        [:span (tr "dashboard.export-shapes")]
+        [:span.shortcut (sc/get-tooltip :export-shapes)]]
        [:li.export-file {:on-click on-export-file}
         [:span (tr "dashboard.export-single")]]
        (when (seq frames)
@@ -397,9 +410,9 @@
 
 (mf/defc header
   [{:keys [file layout project page-id] :as props}]
-  (let [team-id  (:team-id project)
-        zoom     (mf/deref refs/selected-zoom)
-        params   {:page-id page-id :file-id (:id file) :section "interactions"}
+  (let [team-id             (:team-id project)
+        zoom                (mf/deref refs/selected-zoom)
+        params              {:page-id page-id :file-id (:id file) :section "interactions"}
 
         go-back
         (mf/use-callback
@@ -429,6 +442,7 @@
      [:div.right-area
       [:div.options-section
        [:& persistence-state-widget]
+       [:& export-progress-widget]
        [:button.document-history
         {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
          :class (when (contains? layout :document-history) "selected")
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs
index 62161131f..0628dd74a 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs
@@ -50,7 +50,9 @@
      :bool    [:& bool/options {:shape shape}]
      nil)
    [:& exports-menu
-    {:shape shape
+    {:ids [(:id shape)]
+     :values (select-keys shape [:exports])
+     :shape shape
      :page-id page-id
      :file-id file-id}]])
 
@@ -82,7 +84,9 @@
                                                      :file-id file-id
                                                      :shapes-with-children shapes-with-children}]
            :else [:& multiple/options {:shapes-with-children shapes-with-children
-                                       :shapes selected-shapes}])]]
+                                       :shapes selected-shapes
+                                       :page-id page-id
+                                       :file-id file-id}])]]
 
        [:& tab-element {:id :prototype
                         :title (tr "workspace.options.prototype")}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
index 8e9fc5a25..0fe548cf9 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
@@ -7,121 +7,138 @@
 (ns app.main.ui.workspace.sidebar.options.menus.exports
   (:require
    [app.common.data :as d]
-   [app.main.data.messages :as dm]
-   [app.main.data.workspace :as udw]
-   [app.main.data.workspace.persistence :as dwp]
-   [app.main.repo :as rp]
+   [app.main.data.exports :as de]
+   [app.main.data.workspace.changes :as dch]
+   [app.main.data.workspace.state-helpers :as wsh]
+   [app.main.refs :as refs]
    [app.main.store :as st]
+   [app.main.ui.export]
    [app.main.ui.icons :as i]
    [app.util.dom :as dom]
-   [app.util.i18n :as i18n :refer  [tr]]
-   [beicon.core :as rx]
+   [app.util.i18n :refer  [tr c]]
    [rumext.alpha :as mf]))
 
-(defn request-export
-  [shape exports]
-  ;; Force a persist before exporting otherwise the exported shape could be outdated
-  (st/emit! ::dwp/force-persist)
-  (rp/query!
-   :export
-   {:page-id (:page-id shape)
-    :file-id  (:file-id shape)
-    :object-id (:id shape)
-    :name (:name shape)
-    :exports exports}))
-
-(defn use-download-export
-  [shape page-id file-id exports]
-  (let [loading? (mf/use-state false)
-
-        filename (cond-> (:name shape)
-                   (and (= (count exports) 1)
-                        (not (empty (:suffix (first exports)))))
-                   (str (:suffix (first exports))))
-
-        on-download-callback
-        (mf/use-callback
-         (mf/deps filename shape exports)
-         (fn [event]
-           (dom/prevent-default event)
-           (swap! loading? not)
-           (->> (request-export (assoc shape :page-id page-id :file-id file-id) exports)
-                (rx/subs
-                 (fn [body]
-                   (dom/trigger-download filename body))
-                 (fn [_error]
-                   (swap! loading? not)
-                   (st/emit! (dm/error (tr "errors.unexpected-error"))))
-                 (fn []
-                   (swap! loading? not))))))]
-    [on-download-callback @loading?]))
+(def exports-attrs
+  "Shape attrs that corresponds to exports. Used in other namespaces."
+  [:exports])
 
 (mf/defc exports-menu
-  [{:keys [shape page-id file-id] :as props}]
-  (let [exports  (:exports shape [])
+  {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "page-id" "file-id"]))]}
+  [{:keys [ids type values page-id file-id] :as props}]
+  (let [exports      (:exports values [])
+
+        state        (mf/deref refs/export)
+        in-progress? (:in-progress state)
+
+        filename     (when (seqable? exports)
+                       (let [shapes (wsh/lookup-shapes @st/state ids)
+                             sname  (-> shapes first :name)
+                             suffix (-> exports first :suffix)]
+                         (cond-> sname
+                           (and (= 1 (count exports)) (some? suffix))
+                           (str suffix))))
 
         scale-enabled?
         (mf/use-callback
-          (fn [export]
-            (#{:png :jpeg} (:type export))))
+         (fn [export]
+           (#{:png :jpeg} (:type export))))
 
-        [on-download loading?] (use-download-export shape page-id file-id exports)
+        on-download
+        (mf/use-fn
+         (mf/deps ids page-id file-id exports)
+         (fn [event]
+           (dom/prevent-default event)
+           (if (= :multiple type)
+             (st/emit! (de/show-workspace-export-dialog {:selected ids}))
 
+             ;; In other all cases we only allowed to have a single
+             ;; shape-id because multiple shape-ids are handled
+             ;; separatelly by the export-modal.
+             (let [defaults {:page-id page-id
+                             :file-id file-id
+                             :name filename
+                             :object-id (first ids)}
+                   exports  (mapv #(merge % defaults) exports)]
+               (if (= 1 (count exports))
+                 (st/emit! (de/request-simple-export {:export (first exports)}))
+                 (st/emit! (de/request-multiple-export {:exports exports :filename filename})))))))
+
+        ;; TODO: maybe move to specific events for avoid to have this logic here?
         add-export
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps ids)
          (fn []
-           (let [xspec {:type :png
-                        :suffix ""
-                        :scale 1}]
-             (st/emit! (udw/update-shape (:id shape)
-                                         {:exports (conj exports xspec)})))))
+           (let [xspec {:type :png :suffix "" :scale 1}]
+             (st/emit! (dch/update-shapes ids
+                                          (fn [shape]
+                                            (assoc shape :exports (into [xspec] (:exports shape)))))))))
+
         delete-export
         (mf/use-callback
-         (mf/deps shape)
-         (fn [index]
-           (let [[before after] (split-at index exports)
-                 exports        (d/concat-vec before (rest after))]
-             (st/emit! (udw/update-shape (:id shape)
-                                         {:exports exports})))))
+         (mf/deps ids)
+         (fn [position]
+           (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
+                                                              (filterv (fn [[idx _]] (not= idx index)))
+                                                              (mapv second)))
+
+                 remove (fn [shape] (update shape :exports remove-fill-by-index position))]
+             (st/emit! (dch/update-shapes ids remove)))))
 
         on-scale-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps ids)
          (fn [index event]
            (let [target  (dom/get-target event)
                  value   (dom/get-value target)
-                 value   (d/parse-double value)
-                 exports (assoc-in exports [index :scale] value)]
-             (st/emit! (udw/update-shape (:id shape)
-                                         {:exports exports})))))
+                 value   (d/parse-double value)]
+             (st/emit! (dch/update-shapes ids
+                                          (fn [shape]
+                                            (assoc-in shape [:exports index :scale] value)))))))
 
         on-suffix-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps ids)
          (fn [index event]
            (let [target  (dom/get-target event)
-                 value   (dom/get-value target)
-                 exports (assoc-in exports [index :suffix] value)]
-             (st/emit! (udw/update-shape (:id shape)
-                                         {:exports exports})))))
+                 value   (dom/get-value target)]
+             (st/emit! (dch/update-shapes ids
+                                          (fn [shape]
+                                            (assoc-in shape [:exports index :suffix] value)))))))
 
         on-type-change
         (mf/use-callback
-         (mf/deps shape)
+         (mf/deps ids)
          (fn [index event]
            (let [target  (dom/get-target event)
                  value   (dom/get-value target)
-                 value   (keyword value)
-                 exports (assoc-in exports [index :type] value)]
-             (st/emit! (udw/update-shape (:id shape)
-                                         {:exports exports})))))]
+                 value   (keyword value)]
+             (st/emit! (dch/update-shapes ids
+                                          (fn [shape]
+                                            (assoc-in shape [:exports index :type] value)))))))
+
+        on-remove-all
+        (mf/use-callback
+         (mf/deps ids)
+         (fn []
+           (st/emit! (dch/update-shapes ids
+                                        (fn [shape]
+                                          (assoc shape :exports []))))))]
 
     [:div.element-set.exports-options
      [:div.element-set-title
-      [:span (tr "workspace.options.export")]
-      [:div.add-page {:on-click add-export} i/close]]
-     (when (seq exports)
+      [:span (tr (if (> (count ids) 1) "workspace.options.export-multiple" "workspace.options.export"))]
+      (when (not (= :multiple exports))
+        [:div.add-page {:on-click add-export} i/close])]
+
+     (cond
+       (= :multiple exports)
+       [:div.element-set-options-group
+        [:div.element-set-label (tr "settings.multiple")]
+        [:div.element-set-actions
+         [:div.element-set-actions-button {:on-click on-remove-all}
+          i/minus]]]
+
+       (seq exports)
        [:div.element-set-content
         (for [[index export] (d/enumerate exports)]
           [:div.element-set-options-group
@@ -146,14 +163,14 @@
             [:option {:value "svg"} "SVG"]
             [:option {:value "pdf"} "PDF"]]
            [:div.delete-icon {:on-click (partial delete-export index)}
-            i/minus]])
-
-        [:div.btn-icon-dark.download-button
-         {:on-click (when-not loading? on-download)
-          :class (dom/classnames
-                  :btn-disabled loading?)
-          :disabled loading?}
-         (if loading?
-           (tr "workspace.options.exporting-object")
-           (tr "workspace.options.export-object"))]])]))
+            i/minus]])])
 
+     (when (or (= :multiple exports) (seq exports))
+       [:div.btn-icon-dark.download-button
+        {:on-click (when-not in-progress? on-download)
+         :class (dom/classnames
+                 :btn-disabled in-progress?)
+         :disabled in-progress?}
+        (if in-progress?
+          (tr "workspace.options.exporting-object")
+          (tr "workspace.options.export-object" (c (count ids))))])]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
index 02900c8ed..7b4754e02 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
@@ -14,6 +14,7 @@
    [app.main.ui.hooks :as hooks]
    [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]]
    [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
+   [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-attrs exports-menu]]
    [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
    [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
    [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
@@ -36,7 +37,8 @@
     :shadow     :children
     :blur       :children
     :stroke     :shape
-    :text       :children}
+    :text       :children
+    :exports    :shape}
 
    :group
    {:measure    :shape
@@ -46,7 +48,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :children
-    :text       :children}
+    :text       :children
+    :exports    :shape}
 
    :path
    {:measure    :shape
@@ -56,7 +59,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :ignore}
+    :text       :ignore
+    :exports    :shape}
 
    :text
    {:measure    :shape
@@ -66,7 +70,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :text}
+    :text       :text
+    :exports    :shape}
 
    :image
    {:measure    :shape
@@ -76,7 +81,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :ignore
-    :text       :ignore}
+    :text       :ignore
+    :exports    :shape}
 
    :rect
    {:measure    :shape
@@ -86,7 +92,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :ignore}
+    :text       :ignore
+    :exports    :shape}
 
    :circle
    {:measure    :shape
@@ -96,7 +103,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :ignore}
+    :text       :ignore
+    :exports    :shape}
 
    :svg-raw
    {:measure    :shape
@@ -106,7 +114,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :ignore}
+    :text       :ignore
+    :exports    :shape}
 
    :bool
    {:measure    :shape
@@ -116,7 +125,8 @@
     :shadow     :shape
     :blur       :shape
     :stroke     :shape
-    :text       :ignore}})
+    :text       :ignore
+    :exports    :shape}})
 
 (def group->attrs
   {:measure    measure-attrs
@@ -126,7 +136,8 @@
    :shadow     shadow-attrs
    :blur       blur-attrs
    :stroke     stroke-attrs
-   :text       ot/attrs})
+   :text       ot/attrs
+   :exports    exports-attrs})
 
 (def shadow-keys [:style :color :offset-x :offset-y :blur :spread])
 
@@ -211,11 +222,13 @@
     (dissoc :content)))
 
 (mf/defc options
-  {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "shapes-with-children"]))]
+  {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "shapes-with-children" "page-id" "file-id"]))]
    ::mf/wrap-props false}
   [props]
   (let [shapes (unchecked-get props "shapes")
         shapes-with-children (unchecked-get props "shapes-with-children")
+        page-id (unchecked-get props "page-id")
+        file-id (unchecked-get props "file-id")
         objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v))))
         show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
 
@@ -235,7 +248,8 @@
          shadow-ids     shadow-values
          blur-ids       blur-values
          stroke-ids     stroke-values
-         text-ids       text-values]
+         text-ids       text-values
+         exports-ids    exports-values]
         (mf/use-memo
          (mf/deps objects-no-measures)
          (fn []
@@ -248,7 +262,8 @@
              (get-attrs shapes objects-no-measures :shadow)
              (get-attrs shapes objects-no-measures :blur)
              (get-attrs shapes objects-no-measures :stroke)
-             (get-attrs shapes objects-no-measures :text)])))]
+             (get-attrs shapes objects-no-measures :text)
+             (get-attrs shapes objects-no-measures :exports)])))]
 
     [:div.options
      (when-not (empty? measure-ids)
@@ -273,4 +288,7 @@
        [:& blur-menu {:type type :ids blur-ids :values blur-values}])
 
      (when-not (empty? text-ids)
-       [:& ot/text-menu {:type type :ids text-ids :values text-values}])]))
+       [:& ot/text-menu {:type type :ids text-ids :values text-values}])
+
+     (when-not (empty? exports-ids)
+       [:& exports-menu {:type type :ids exports-ids :values exports-values :page-id page-id :file-id file-id}])]))
diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs
index 3ba2280fc..5c890ac89 100644
--- a/frontend/src/app/util/http.cljs
+++ b/frontend/src/app/util/http.cljs
@@ -131,7 +131,7 @@
 (defn transit-data
   [data]
   (reify IBodyData
-    (-get-body-data [_] (t/encode-str data))
+    (-get-body-data [_] (t/encode-str data {:type :json-verbose}))
     (-update-headers [_ headers]
       (assoc headers "content-type" "application/transit+json"))))
 
diff --git a/frontend/src/app/util/websocket.cljs b/frontend/src/app/util/websocket.cljs
new file mode 100644
index 000000000..bb6506f03
--- /dev/null
+++ b/frontend/src/app/util/websocket.cljs
@@ -0,0 +1,119 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.util.websocket
+  "A interface to webworkers exposed functionality."
+  (:require
+   [app.common.transit :as t]
+   [beicon.core :as rx]
+   [goog.events :as ev])
+  (:import
+   goog.net.WebSocket
+   goog.net.WebSocket.EventType))
+
+(defprotocol IWebSocket
+  (-stream [_] "Retrieve the message stream")
+  (-send [_ message] "send a message")
+  (-close [_] "close websocket")
+  (-open? [_] "check if the channel is open"))
+
+(defn create
+  [uri]
+  (let [sb   (rx/subject)
+        ws   (WebSocket. #js {:autoReconnect true})
+        data (atom {})
+        lk1  (ev/listen ws EventType.MESSAGE
+                        #(rx/push! sb {:type :message :payload (.-message %)}))
+        lk2  (ev/listen ws EventType.ERROR
+                        #(rx/push! sb {:type :error :payload %}))
+        lk3  (ev/listen ws EventType.OPENED
+                        #(rx/push! sb {:type :opened :payload %}))]
+
+    (.open ws (str uri))
+    (reify
+      IDeref
+      (-deref [_] (-deref data))
+
+      IReset
+      (-reset! [_ newval]
+        (-reset! data newval))
+
+      ISwap
+      (-swap! [_ f]
+        (-swap! data f))
+      (-swap! [_ f x]
+        (-swap! data f x))
+      (-swap! [_ f x y]
+        (-swap! data f x y))
+      (-swap! [_ f x y more]
+        (-swap! data f x y more))
+
+      IWatchable
+      (-notify-watches [_ oldval newval]
+        (-notify-watches data oldval newval))
+
+      (-add-watch [_ key f]
+        (-add-watch data key f))
+
+      (-remove-watch [_ key]
+        (-remove-watch data key))
+
+      IHash
+      (-hash [_] (goog/getUid ws))
+
+      IWebSocket
+      (-stream [_]
+        (->> sb
+             (rx/map (fn [{:keys [type payload] :as message}]
+                       (cond-> message
+                         (= :message type)
+                         (assoc :payload (t/decode-str payload)))))))
+
+      (-send [_ msg]
+        (when (.isOpen ^js ws)
+          (.send ^js ws msg)))
+
+      (-open? [_]
+        (.isOpen ^js ws))
+
+      (-close [_]
+        (rx/end! sb)
+        (ev/unlistenByKey lk1)
+        (ev/unlistenByKey lk2)
+        (ev/unlistenByKey lk3)
+        (.close ^js ws)
+        (.dispose ^js ws)))))
+
+(defn message-event?
+  ^boolean
+  [msg]
+  (= (:type msg) :message))
+
+(defn error-event?
+  ^boolean
+  [msg]
+  (= (:type msg) :error))
+
+(defn opened-event?
+  ^boolean
+  [msg]
+  (= (:type msg) :opened))
+
+(defn send!
+  [ws msg]
+  (-send ws (t/encode-str msg)))
+
+(defn close!
+  [ws]
+  (-close ws))
+
+(defn open?
+  [ws]
+  (-open? ws))
+
+(defn get-rcv-stream
+  [ws]
+  (-stream ws))
diff --git a/frontend/src/app/util/websockets.cljs b/frontend/src/app/util/websockets.cljs
deleted file mode 100644
index 14351085e..000000000
--- a/frontend/src/app/util/websockets.cljs
+++ /dev/null
@@ -1,57 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.util.websockets
-  "A interface to webworkers exposed functionality."
-  (:require
-   [app.common.transit :as t]
-   [beicon.core :as rx]
-   [goog.events :as ev])
-  (:import
-   goog.net.WebSocket
-   goog.net.WebSocket.EventType))
-
-(defprotocol IWebSocket
-  (-stream [_] "Retrieve the message stream")
-  (-send [_ message] "send a message")
-  (-close [_] "close websocket"))
-
-(defn open
-  [uri]
-  (let [sb (rx/subject)
-        ws (WebSocket. #js {:autoReconnect true})
-        lk1 (ev/listen ws EventType.MESSAGE
-                       #(rx/push! sb {:type :message :payload (.-message %)}))
-        lk2 (ev/listen ws EventType.ERROR
-                       #(rx/push! sb {:type :error :payload %}))
-        lk3 (ev/listen ws EventType.OPENED
-                       #(rx/push! sb {:type :opened :payload %}))]
-    (.open ws (str uri))
-    (reify
-      cljs.core/IDeref
-      (-deref [_] ws)
-
-      IWebSocket
-      (-stream [_] sb)
-      (-send [_ msg]
-        (when (.isOpen ^js ws)
-          (.send ^js ws msg)))
-      (-close [_]
-        (rx/end! sb)
-        (ev/unlistenByKey lk1)
-        (ev/unlistenByKey lk2)
-        (ev/unlistenByKey lk3)
-        (.close ^js ws)
-        (.dispose ^js ws)))))
-
-
-(defn message?
-  [msg]
-  (= (:type msg) :message))
-
-(defn send!
-  [ws msg]
-  (-send ws (t/encode-str msg)))
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 7e7dbb6fc..a06a30223 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -256,6 +256,10 @@ msgstr ""
 "Oh no! You have no files yet! If you want to try with some templates go "
 "to [Libraries & templates](https://penpot.app/libraries-templates.html)"
 
+#: src/app/main/ui/workspace/header.cljs
+msgid "dashboard.export-shapes"
+msgstr "Exportar"
+
 msgid "dashboard.export-frames"
 msgstr "Export artboards to PDF..."
 
@@ -300,6 +304,26 @@ msgstr "Include shared library assets in file libraries"
 msgid "dashboard.export.title"
 msgstr "Export files"
 
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.title"
+msgstr "Export selection"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.selected"
+msgstr "%s de %s elements selected"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.no-elements"
+msgstr "There are no elements with export settings."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.how-to"
+msgstr "You can add export settings to elements from the design properties (at the bottom of the right sidebar)."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.how-to-link"
+msgstr "Info how to set exports at Penpot."
+
 msgid "dashboard.fonts.deleted-placeholder"
 msgstr "Font deleted"
 
@@ -2480,18 +2504,40 @@ msgstr "Design"
 msgid "workspace.options.export"
 msgstr "Export"
 
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.export-multiple"
+msgstr "Export selection"
+
 #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
 msgid "workspace.options.export-object"
-msgstr "Export"
+msgid_plural "workspace.options.export-object"
+msgstr[0] "Export 1 element"
+msgstr[1] "Export %s elements"
 
 #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
 msgid "workspace.options.export.suffix"
 msgstr "Suffix"
 
-#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
 msgid "workspace.options.exporting-object"
 msgstr "Exporting…"
 
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.exporting-object-slow"
+msgstr "Export unexpectedly slow"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.exporting-object-error"
+msgstr "Export failed"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.exporting-object-slow"
+msgstr "Export unexpectedly slow"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.retry"
+msgstr "Retry"
+
 #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
 msgid "workspace.options.fill"
 msgstr "Fill"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 36ae34aa4..05437697a 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -260,6 +260,10 @@ msgstr ""
 "¡Oh, no! ¡Aún no tienes archivos! Si quieres probar con alguna plantilla ve a "
 "[Bibliotecas y plantillas](https://penpot.app/libraries-templates.html)"
 
+#: src/app/main/ui/workspace/header.cljs
+msgid "dashboard.export-shapes"
+msgstr "Exportar"
+
 msgid "dashboard.export-frames"
 msgstr "Exportar tableros a PDF..."
 
@@ -304,6 +308,26 @@ msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
 msgid "dashboard.export.title"
 msgstr "Exportar ficheros"
 
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.title"
+msgstr "Exportar selección"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.selected"
+msgstr "%s de %s elementos seleccionados"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.no-elements"
+msgstr "No hay elementos con configuraciones de exportación."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.how-to"
+msgstr " Puedes añadir configuraciones de exportación a elementos desde las propiedades de diseño (al final del lateral derecho)."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "dashboard.export-shapes.how-to-link"
+msgstr "Información sobre cómo configurar exportaciones en Penpot."
+
 msgid "dashboard.fonts.deleted-placeholder"
 msgstr "Fuente eliminada."
 
@@ -2496,17 +2520,35 @@ msgstr "Diseño"
 msgid "workspace.options.export"
 msgstr "Exportar"
 
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.export-multiple"
+msgstr "Exportar selección"
+
 #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
 msgid "workspace.options.export-object"
-msgstr "Exportar"
+msgid_plural "workspace.options.export-object"
+msgstr[0] "Exportar 1 elemento"
+msgstr[1] "Exportar %s elementos"
 
 #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
 msgid "workspace.options.export.suffix"
 msgstr "Sufijo"
 
-#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
 msgid "workspace.options.exporting-object"
-msgstr "Exportando"
+msgstr "Exportando..."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.exporting-object-slow"
+msgstr "Exportación lenta"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.retry"
+msgstr "Reintentar"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
+msgid "workspace.options.exporting-object-error"
+msgstr "Exportación fallida"
 
 #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
 msgid "workspace.options.fill"