diff --git a/CHANGES.md b/CHANGES.md
index 5adb6f627..f9b5f8d72 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,7 @@
 ### :sparkles: New features
 
 - Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
+- Override browser Ctrl+ and Ctrl- zoom with Penpot Zoom [Taiga #3200](https://tree.taiga.io/project/penpot/us/3200)
 
 ### :bug: Bugs fixed
 
diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index 46919efa4..3c3f71f74 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -20,6 +20,7 @@
    [app.main.data.workspace.viewport :as dwv]
    [app.main.repo :as rp]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.router :as rt]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -37,7 +38,8 @@
         (rx/merge
          (rx/of (dcm/retrieve-comment-threads file-id))
          (->> stream
-              (rx/filter ms/mouse-click?)
+              (rx/filter mse/mouse-event?)
+              (rx/filter mse/mouse-click-event?)
               (rx/switch-map #(rx/take 1 ms/mouse-position))
               (rx/with-latest-from ms/keyboard-space)
               (rx/filter (fn [[_ space]] (not space)) )
diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs
index 7df56e1f0..d8ccdf4c9 100644
--- a/frontend/src/app/main/data/workspace/drawing/box.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/box.cljs
@@ -24,6 +24,7 @@
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.snap :as snap]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -76,7 +77,13 @@
   (ptk/reify ::handle-drawing
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stoper       (rx/filter #(or (ms/mouse-up? %) (= % :interrupt))  stream)
+      (let [stoper       (rx/merge
+                          (->> stream
+                               (rx/filter mse/mouse-event?)
+                               (rx/filter mse/mouse-up-event?))
+                          (->> stream
+                               (rx/filter #(= % :interrupt))))
+
             layout       (get state :workspace-layout)
             zoom         (dm/get-in state [:workspace-local :zoom] 1)
 
diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs
index d0b1aed44..f77078281 100644
--- a/frontend/src/app/main/data/workspace/drawing/curve.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs
@@ -21,6 +21,7 @@
    [app.main.data.workspace.drawing.common :as common]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.path.simplify-curve :as ups]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -29,7 +30,8 @@
 
 (defn stoper-event?
   [{:keys [type] :as event}]
-  (ms/mouse-event? event) (= type :up))
+  (and (mse/mouse-event? event)
+       (= type :up)))
 
 (defn- insert-point
   [point]
diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs
index 622eeb70b..a7610d2e5 100644
--- a/frontend/src/app/main/data/workspace/interactions.cljs
+++ b/frontend/src/app/main/data/workspace/interactions.cljs
@@ -19,6 +19,7 @@
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.data.workspace.undo :as dwu]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -187,7 +188,9 @@
     (watch [_ state stream]
       (let [initial-pos @ms/mouse-position
             selected (wsh/lookup-selected state)
-            stopper (rx/filter ms/mouse-up? stream)]
+            stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))]
         (when (= 1 (count selected))
           (rx/concat
             (->> ms/mouse-position
@@ -295,7 +298,9 @@
     (watch [_ state stream]
       (let [initial-pos @ms/mouse-position
             selected (wsh/lookup-selected state)
-            stopper (rx/filter ms/mouse-up? stream)]
+            stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))]
         (when (= 1 (count selected))
           (let [page-id     (:current-page-id state)
                 objects     (wsh/lookup-page-objects state page-id)
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index 4b5916055..8a88ddf70 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -16,8 +16,8 @@
    [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.globals :refer [global]]
+   [app.util.mouse :as mse]
    [app.util.object :as obj]
    [app.util.time :as dt]
    [beicon.core :as rx]
@@ -81,7 +81,7 @@
                              ;; Emit to all other connected users the current pointer
                              ;; position changes.
                              (->> stream
-                                  (rx/filter ms/pointer-event?)
+                                  (rx/filter mse/pointer-event?)
                                   (rx/sample 50)
                                   (rx/map #(handle-pointer-send file-id (:pt %)))))
 
diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs
index a0dd03249..912a90718 100644
--- a/frontend/src/app/main/data/workspace/path/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/path/drawing.cljs
@@ -26,7 +26,7 @@
    [app.main.data.workspace.path.streams :as streams]
    [app.main.data.workspace.path.undo :as undo]
    [app.main.data.workspace.state-helpers :as wsh]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -122,16 +122,12 @@
 
 (declare close-path-drag-end)
 
-(defn close-path-drag-start [position]
+(defn close-path-drag-start
+  [position]
   (ptk/reify ::close-path-drag-start
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stop-stream
-            (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                        (ms/mouse-up? %))))
-
-            content (st/get-path state :content)
-
+      (let [content  (st/get-path state :content)
             handlers (-> (upc/content->handlers content)
                          (get position))
 
@@ -140,8 +136,14 @@
 
             drag-events-stream
             (->> (streams/position-stream)
-                 (rx/take-until stop-stream)
-                 (rx/map #(drag-handler position idx prefix %)))]
+                 (rx/map #(drag-handler position idx prefix %))
+                 (rx/take-until
+                  (rx/merge
+                   (->> stream
+                        (rx/filter mse/mouse-event?)
+                        (rx/filter mse/mouse-up-event?))
+                   (->> stream
+                        (rx/filter helpers/end-path-event?)))))]
 
         (rx/concat
          (rx/of (add-node position))
@@ -163,12 +165,16 @@
   (ptk/reify ::start-path-from-point
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [mouse-up    (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                                    (ms/mouse-up? %))))
-            drag-events (->> (streams/position-stream)
-                             (rx/take-until mouse-up)
-                             (rx/map #(drag-handler %)))]
+      (let [stoper (rx/merge
+                    (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> stream
+                         (rx/filter helpers/end-path-event?)))
 
+            drag-events (->> (streams/position-stream)
+                             (rx/map #(drag-handler %))
+                             (rx/take-until stoper))]
         (rx/concat
          (rx/of (add-node position))
          (streams/drag-stream
@@ -185,13 +191,16 @@
 
 (defn make-drag-stream
   [stream down-event]
-  (let [mouse-up    (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                                (ms/mouse-up? %))))
+  (let [stoper (rx/merge
+                (->> stream
+                     (rx/filter mse/mouse-event?)
+                     (rx/filter mse/mouse-up-event?))
+                (->> stream
+                     (rx/filter helpers/end-path-event?)))
 
         drag-events (->> (streams/position-stream)
-                         (rx/take-until mouse-up)
-                         (rx/map #(drag-handler %)))]
-
+                         (rx/map #(drag-handler %))
+                         (rx/take-until stoper))]
     (rx/concat
      (rx/of (add-node down-event))
      (streams/drag-stream
@@ -209,8 +218,11 @@
 
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [mouse-down      (->> stream (rx/filter ms/mouse-down?))
-            end-path-events (->> stream (rx/filter helpers/end-path-event?))
+      (let [mouse-down      (->> stream
+                                 (rx/filter mse/mouse-event?)
+                                 (rx/filter mse/mouse-down-event?))
+            end-path-events (->> stream
+                                 (rx/filter helpers/end-path-event?))
 
             ;; Mouse move preview
             mousemove-events
diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs
index f1a9c8bc9..31080603c 100644
--- a/frontend/src/app/main/data/workspace/path/edition.cljs
+++ b/frontend/src/app/main/data/workspace/path/edition.cljs
@@ -26,6 +26,7 @@
    [app.main.data.workspace.path.undo :as undo]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.path.tools :as upt]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -150,7 +151,10 @@
   (ptk/reify ::drag-selected-points
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stopper (->> stream (rx/filter ms/mouse-up?))
+      (let [stopper (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+
             id (dm/get-in state [:workspace-local :edition])
 
             selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
@@ -263,8 +267,6 @@
          (rx/concat
           (rx/of (dch/update-shapes [id] upsp/convert-to-path))
           (->> (streams/move-handler-stream handler point handler opposite points)
-               (rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %)
-                                                          (streams/finish-edition? %)))))
                (rx/map
                 (fn [{:keys [x y alt? shift?]}]
                   (let [pos (cond-> (gpt/point x y)
@@ -275,7 +277,15 @@
                      prefix
                      (+ start-delta-x (- (:x pos) (:x handler)))
                      (+ start-delta-y (- (:y pos) (:y handler)))
-                     (not alt?))))))
+                     (not alt?)))))
+               (rx/take-until
+                (rx/merge
+                 (->> stream
+                      (rx/filter mse/mouse-event?)
+                      (rx/filter mse/mouse-up-event?))
+                 (->> stream
+                      (rx/filter streams/finish-edition?)))))
+
           (rx/concat (rx/of (apply-content-modifiers)))))))))
 
 (declare stop-path-edit)
diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs
index 5792a69f8..4249c23fe 100644
--- a/frontend/src/app/main/data/workspace/path/helpers.cljs
+++ b/frontend/src/app/main/data/workspace/path/helpers.cljs
@@ -14,16 +14,19 @@
    [app.common.svg.path.command :as upc]
    [app.common.svg.path.subpath :as ups]
    [app.main.data.workspace.path.common :as common]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [potok.core :as ptk]))
 
-(defn end-path-event? [event]
-  (or (= (ptk/type event) ::common/finish-path)
-      (= (ptk/type event) :app.main.data.workspace.path.shortcuts/esc-pressed)
-      (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))
-      (= :app.main.data.workspace/finalize-page (ptk/type event))
-      (= event :interrupt) ;; ESC
-      (ms/mouse-double-click? event)))
+(defn end-path-event?
+  [event]
+  (let [type (ptk/type event)]
+    (or (= type ::common/finish-path)
+        (= type :app.main.data.workspace.path.shortcuts/esc-pressed)
+        (= type :app.main.data.workspace.common/clear-edition-mode)
+        (= type :app.main.data.workspace/finalize-page)
+        (= event :interrupt) ;; ESC
+        (and ^boolean (mse/mouse-event? event)
+             ^boolean (mse/mouse-double-click-event? event)))))
 
 (defn content-center
   [content]
diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs
index 4d517eaf6..24b7b0bf4 100644
--- a/frontend/src/app/main/data/workspace/path/selection.cljs
+++ b/frontend/src/app/main/data/workspace/path/selection.cljs
@@ -13,6 +13,7 @@
    [app.main.data.workspace.common :as dwc]
    [app.main.data.workspace.path.state :as st]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -118,16 +119,21 @@
     (ptk/reify ::handle-area-selection
       ptk/WatchEvent
       (watch [_ state stream]
-        (let [zoom (get-in state [:workspace-local :zoom] 1)
-              stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event)))
-              stoper (->> stream (rx/filter stop?))
+        (let [zoom   (get-in state [:workspace-local :zoom] 1)
+              stoper (rx/merge
+                      (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
+                      (->> stream
+                           (rx/filter dwc/interrupt?)))
+
               from-p @ms/mouse-position]
           (rx/concat
            (->> ms/mouse-position
-                (rx/take-until stoper)
                 (rx/map #(grc/points->rect [from-p %]))
                 (rx/filter (partial valid-rect? zoom))
-                (rx/map update-area-selection))
+                (rx/map update-area-selection)
+                (rx/take-until stoper))
 
            (rx/of (select-node-area shift?)
                   (clear-area-selection))))))))
diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs
index 071e6ab12..264a145a1 100644
--- a/frontend/src/app/main/data/workspace/path/streams.cljs
+++ b/frontend/src/app/main/data/workspace/path/streams.cljs
@@ -14,6 +14,7 @@
    [app.main.snap :as snap]
    [app.main.store :as st]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [okulary.core :as l]
    [potok.core :as ptk]))
@@ -50,16 +51,20 @@
    (let [zoom  (get-in @st/state [:workspace-local :zoom] 1)
 
          start (-> @ms/mouse-position to-pixel-snap)
-         mouse-up (->> st/stream
-                       (rx/filter #(or (finish-edition? %)
-                                       (ms/mouse-up? %))))
+
+         stoper (rx/merge
+                    (->> st/stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> st/stream
+                         (rx/filter finish-edition?)))
 
          position-stream
          (->> ms/mouse-position
-              (rx/take-until mouse-up)
               (rx/map to-pixel-snap)
               (rx/filter (dragging? start zoom))
-              (rx/take 1))]
+              (rx/take 1)
+              (rx/take-until stoper))]
 
      (rx/merge
       (->> position-stream
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index fdaaaeb99..aac220262 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -35,12 +35,15 @@
    [app.main.refs :as refs]
    [app.main.streams :as ms]
    [app.main.worker :as uw]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [clojure.set :as set]
    [linked.set :as lks]
    [potok.core :as ptk]))
 
-(defn interrupt? [e] (= e :interrupt))
+(defn interrupt?
+  [e]
+  (= e :interrupt))
 
 ;; --- Selection Rect
 
@@ -60,8 +63,12 @@
     ptk/WatchEvent
     (watch [_ state stream]
       (let [zoom   (dm/get-in state [:workspace-local :zoom] 1)
-            stop?  (fn [event] (or (interrupt? event) (ms/mouse-up? event)))
-            stoper (rx/filter stop? stream)
+            stoper (rx/merge
+                    (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> stream
+                         (rx/filter interrupt?)))
 
             init-position @ms/mouse-position
 
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 951962aa3..9f3434396 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -33,6 +33,8 @@
    [app.main.snap :as snap]
    [app.main.streams :as ms]
    [app.util.dom :as dom]
+   [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -214,7 +216,9 @@
       ptk/WatchEvent
       (watch [_ state stream]
         (let [initial-position @ms/mouse-position
-              stopper (rx/filter ms/mouse-up? stream)
+              stopper (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
               layout  (:workspace-layout state)
               page-id (:current-page-id state)
               focus   (:workspace-focus-selected state)
@@ -305,7 +309,10 @@
 
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [stoper          (rx/filter ms/mouse-up? stream)
+      (let [stoper          (->> stream
+                                 (rx/filter mse/mouse-event?)
+                                 (rx/filter mse/mouse-up-event?))
+
             group           (gsh/shapes->rect shapes)
             group-center    (grc/rect->center group)
             initial-angle   (gpt/angle @ms/mouse-position group-center)
@@ -369,7 +376,10 @@
      (watch [_ state stream]
        (let [initial  (deref ms/mouse-position)
 
-             stopper  (rx/filter ms/mouse-up? stream)
+             stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
+
              zoom    (get-in state [:workspace-local :zoom] 1)
 
              ;; We toggle the selection so we don't have to wait for the event
@@ -443,7 +453,11 @@
              ids     (if (nil? ids) selected ids)
              shapes  (mapv #(get objects %) ids)
              duplicate-move-started? (get-in state [:workspace-local :duplicate-move-started?] false)
-             stopper (rx/filter ms/mouse-up? stream)
+
+             stopper (->> stream
+                          (rx/filter mse/mouse-event?)
+                          (rx/filter mse/mouse-up-event?))
+
              layout  (get state :workspace-layout)
              zoom    (get-in state [:workspace-local :zoom] 1)
              focus   (:workspace-focus-selected state)
@@ -673,7 +687,8 @@
                      (rx/switch-map #(rx/merge
                                       (rx/timer 1000)
                                       (->> stream
-                                           (rx/filter ms/key-up?)
+                                           (rx/filter kbd/keyboard-event?)
+                                           (rx/filter kbd/key-up-event?)
                                            (rx/delay 250))))
                      (rx/take 1))
 
diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs
index dacc3ec22..1bbb48ccf 100644
--- a/frontend/src/app/main/data/workspace/viewport.cljs
+++ b/frontend/src/app/main/data/workspace/viewport.cljs
@@ -15,7 +15,7 @@
    [app.common.geom.shapes :as gsh]
    [app.common.math :as mth]
    [app.main.data.workspace.state-helpers :as wsh]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -155,7 +155,7 @@
           (rx/concat
            (rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
            (->> stream
-                (rx/filter ms/pointer-event?)
+                (rx/filter mse/pointer-event?)
                 (rx/filter #(= :delta (:source %)))
                 (rx/map :pt)
                 (rx/take-until stopper)
diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs
index 0117631fa..8aba81a0b 100644
--- a/frontend/src/app/main/data/workspace/zoom.cljs
+++ b/frontend/src/app/main/data/workspace/zoom.cljs
@@ -14,6 +14,7 @@
    [app.common.geom.shapes :as gsh]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -118,7 +119,7 @@
           (rx/concat
            (rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
            (->> stream
-                (rx/filter ms/pointer-event?)
+                (rx/filter mse/pointer-event?)
                 (rx/filter #(= :delta (:source %)))
                 (rx/map :pt)
                 (rx/take-until stopper)
diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs
index a4dc59d69..b17f5808d 100644
--- a/frontend/src/app/main/streams.cljs
+++ b/frontend/src/app/main/streams.cljs
@@ -11,94 +11,43 @@
    [app.main.store :as st]
    [app.util.globals :as globals]
    [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]))
 
 ;; --- User Events
 
-(defrecord KeyboardEvent [type key shift ctrl alt meta editing])
-
-(defn keyboard-event?
-  [v]
-  (instance? KeyboardEvent v))
-
-(defn key-up?
-  [v]
-  (and (keyboard-event? v)
-       (= :up (:type v))))
-
-(defn key-down?
-  [v]
-  (and (keyboard-event? v)
-       (= :down (:type v))))
-
-(defrecord MouseEvent [type ctrl shift alt meta])
-
-(defn mouse-event?
-  [v]
-  (instance? MouseEvent v))
-
-(defn mouse-down?
-  [v]
-  (and (mouse-event? v)
-       (= :down (:type v))))
-
-(defn mouse-up?
-  [v]
-  (and (mouse-event? v)
-       (= :up (:type v))))
-
-(defn mouse-click?
-  [v]
-  (and (mouse-event? v)
-       (= :click (:type v))))
-
-(defn mouse-double-click?
-  [v]
-  (and (mouse-event? v)
-       (= :double-click (:type v))))
-
-(defrecord PointerEvent [source pt ctrl shift alt meta])
-
-(defn pointer-event?
-  [v]
-  (instance? PointerEvent v))
-
-(defrecord ScrollEvent [point])
-
-(defn scroll-event?
-  [v]
-  (instance? ScrollEvent v))
-
 (defn interaction-event?
   [event]
-  (or (keyboard-event? event)
-      (mouse-event? event)))
+  (or ^boolean (kbd/keyboard-event? event)
+      ^boolean (mse/mouse-event? event)))
 
 ;; --- Derived streams
 
+(defonce ^:private pointer
+  (->> st/stream
+       (rx/filter mse/pointer-event?)
+       (rx/share)))
+
 (defonce mouse-position
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/filter #(= :viewport (:source %)))
-                 (rx/map :pt))]
+        ob  (->> pointer
+                 (rx/filter #(= :viewport (mse/get-pointer-source %)))
+                 (rx/map mse/get-pointer-position))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-ctrl
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :ctrl)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-ctrl-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-meta
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :meta)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-meta-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
@@ -110,71 +59,68 @@
 
 (defonce mouse-position-shift
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :shift)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-shift-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-alt
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :alt)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-alt-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
-
-(defonce window-blur
+(defonce ^:private window-blur
   (->> (rx/from-event globals/window "blur")
+       (rx/map (constantly false))
+       (rx/share)))
+
+(defonce keyboard
+  (->> st/stream
+       (rx/filter kbd/keyboard-event?)
        (rx/share)))
 
 (defonce keyboard-alt
   (let [sub (rx/behavior-subject nil)
-        ob  (->> (rx/merge
-                  (->> st/stream
-                       (rx/filter keyboard-event?)
-                       (rx/filter kbd/alt-key?)
-                       (rx/map #(= :down (:type %))))
-                  ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts,
-                  ;; that makes keyboard-alt stream registering the key pressed but
-                  ;; on blurring the window (unfocus) the key down is never arrived.
-                  (->> window-blur
-                       (rx/map (constantly false))))
+        ob  (->> keyboard
+                 (rx/filter kbd/alt-key?)
+                 (rx/map kbd/key-down-event?)
+                 ;; Fix a situation caused by using `ctrl+alt` kind of
+                 ;; shortcuts, that makes keyboard-alt stream
+                 ;; registering the key pressed but on blurring the
+                 ;; window (unfocus) the key down is never arrived.
+                 (rx/merge window-blur)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce keyboard-ctrl
   (let [sub (rx/behavior-subject nil)
-        ob  (->> (rx/merge
-                  (->> st/stream
-                       (rx/filter keyboard-event?)
-                       (rx/filter kbd/ctrl-key?)
-                       (rx/map #(= :down (:type %))))
-                  ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts,
-                  ;; that makes keyboard-alt stream registering the key pressed but
-                  ;; on blurring the window (unfocus) the key down is never arrived.
-                  (->> window-blur
-                       (rx/map (constantly false))))
+        ob  (->> keyboard
+                 (rx/filter kbd/ctrl-key?)
+                 (rx/map kbd/key-down-event?)
+                 ;; Fix a situation caused by using `ctrl+alt` kind of
+                 ;; shortcuts, that makes keyboard-alt stream
+                 ;; registering the key pressed but on blurring the
+                 ;; window (unfocus) the key down is never arrived.
+                 (rx/merge window-blur)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce keyboard-meta
   (let [sub (rx/behavior-subject nil)
-        ob  (->> (rx/merge
-                  (->> st/stream
-                       (rx/filter keyboard-event?)
-                       (rx/filter kbd/meta-key?)
-                       (rx/map #(= :down (:type %))))
-                  ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts,
-                  ;; that makes keyboard-alt stream registering the key pressed but
-                  ;; on blurring the window (unfocus) the key down is never arrived.
-                  (->> window-blur
-                       (rx/map (constantly false))))
+        ob  (->> keyboard
+                 (rx/filter kbd/meta-key?)
+                 (rx/map kbd/key-down-event?)
+                 ;; Fix a situation caused by using `ctrl+alt` kind of
+                 ;; shortcuts, that makes keyboard-alt stream
+                 ;; registering the key pressed but on blurring the
+                 ;; window (unfocus) the key down is never arrived.
+                 (rx/merge window-blur)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
@@ -186,33 +132,10 @@
 
 (defonce keyboard-space
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter keyboard-event?)
+        ob  (->> keyboard
                  (rx/filter kbd/space?)
-                 (rx/filter (comp not kbd/editing?))
-                 (rx/map #(= :down (:type %)))
-                 (rx/dedupe))]
-    (rx/subscribe-with ob sub)
-    sub))
-
-(defonce keyboard-z
-  (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter keyboard-event?)
-                 (rx/filter kbd/z?)
-                 (rx/filter (comp not kbd/editing?))
-                 (rx/map #(= :down (:type %)))
-                 (rx/dedupe))]
-    (rx/subscribe-with ob sub)
-    sub))
-
-(defonce keyboard-shift
-  (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter keyboard-event?)
-                 (rx/filter kbd/shift-key?)
-                 (rx/filter (comp not kbd/editing?))
-                 (rx/map #(= :down (:type %)))
+                 (rx/filter (complement kbd/editing-event?))
+                 (rx/map kbd/key-down-event?)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs
index dd0babf9f..c4a653320 100644
--- a/frontend/src/app/main/ui/hooks.cljs
+++ b/frontend/src/app/main/ui/hooks.cljs
@@ -348,34 +348,36 @@
     state))
 
 (defn use-dynamic-grid-item-width
-  ([]
-   (use-dynamic-grid-item-width nil))
-
+  ([] (use-dynamic-grid-item-width nil))
   ([itemsize]
-   (let [width      (mf/use-state (:items-width @storage))
-         rowref     (mf/use-ref)
+   (let [;; NOTE: we pass a function to use-state for avoid repeatedly
+         ;; lookup `:items-width` on storage on each render
+         width*   (mf/use-state #(:items-width @storage))
+         width    (deref width*)
 
-         itemsize   (cond
-                      (some? itemsize) itemsize
-                      (>= @width 1030) 280
-                      :else            230)
+         rowref   (mf/use-ref)
 
-         ratio      (if (some? @width) (/ @width itemsize) 0)
-         nitems     (mth/floor ratio)
-         limit      (min 10 nitems)
-         limit      (max 1 limit)]
+         itemsize (cond
+                    (some? itemsize) itemsize
+                    (>= width 1030)  280
+                    :else            230)
 
-     (mf/with-effect
+         ratio    (if (some? width) (/ width itemsize) 0)
+         nitems   (mth/floor ratio)
+         limit    (mth/min 10 nitems)
+         limit    (mth/max 1 limit)]
+
+     (mf/with-effect []
        (let [node (mf/ref-val rowref)
              mnt? (volatile! true)
              sub  (->> (wapi/observe-resize node)
                        (rx/observe-on :af)
                        (rx/subs (fn [entries]
-                                  (let [row (first entries)
-                                        row-rect (.-contentRect ^js row)
+                                  (let [row       (first entries)
+                                        row-rect  (.-contentRect ^js row)
                                         row-width (.-width ^js row-rect)]
                                     (when @mnt?
-                                      (reset! width row-width)
+                                      (reset! width* row-width)
                                       (swap! storage assoc :items-width row-width))))))]
          (fn []
            (vreset! mnt? false)
diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
index c9a699db3..82b983c0b 100644
--- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
@@ -21,12 +21,12 @@
    [app.main.data.workspace.specialized-panel :as-alias dwsp]
    [app.main.refs :as refs]
    [app.main.store :as st]
-   [app.main.streams :as ms]
    [app.main.ui.workspace.viewport.viewport-ref :as uwvv]
    [app.util.dom :as dom]
    [app.util.dom.dnd :as dnd]
    [app.util.dom.normalize-wheel :as nw]
    [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [app.util.object :as obj]
    [app.util.timers :as timers]
    [app.util.webapi :as wapi]
@@ -85,7 +85,7 @@
 
              left-click?
              (do
-               (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?)
+               (st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
                          ::dwsp/interrupt)
 
                (when (and (not= edition id) (or text-editing? grid-editing?))
@@ -173,7 +173,7 @@
              hovering? (some? @hover)
              raw-pt (dom/get-client-position event)
              pt     (uwvv/point->viewport raw-pt)]
-         (st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?))
+         (st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
 
          (when (and hovering?
                     (not @space?)
@@ -213,7 +213,7 @@
 
              grid-layout-id (->> @hover-ids reverse (d/seek (partial ctl/grid-layout? objects)))]
 
-         (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt? meta?))
+         (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?))
 
          ;; Emit asynchronously so the double click to exit shapes won't break
          (timers/schedule
@@ -283,7 +283,7 @@
            middle-click? (= 2 (.-which event))]
 
        (when left-click?
-         (st/emit! (ms/->MouseEvent :up ctrl? shift? alt? meta?)))
+         (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)))
 
        (when middle-click?
          (dom/prevent-default event)
@@ -314,29 +314,33 @@
            shift?   (kbd/shift? event)
            alt?     (kbd/alt? event)
            meta?    (kbd/meta? event)
+           mod?     (kbd/mod? event)
            target   (dom/get-target event)
+
            editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
                         (= "rich-text" (obj/get target "className"))
                         (= "INPUT" (obj/get target "tagName"))
                         (= "TEXTAREA" (obj/get target "tagName")))]
 
        (when-not (.-repeat bevent)
-         (st/emit! (ms/->KeyboardEvent :down key shift? ctrl? alt? meta? editing?)))))))
+         (st/emit! (kbd/->KeyboardEvent :down key shift? ctrl? alt? meta? mod? editing? event)))))))
 
 (defn on-key-up []
   (mf/use-callback
    (fn [event]
-     (let [key    (.-key event)
-           ctrl?  (kbd/ctrl? event)
-           shift? (kbd/shift? event)
-           alt?   (kbd/alt? event)
-           meta?  (kbd/meta? event)
+     (let [key      (.-key event)
+           ctrl?    (kbd/ctrl? event)
+           shift?   (kbd/shift? event)
+           alt?     (kbd/alt? event)
+           meta?    (kbd/meta? event)
+           mod?     (kbd/mod? event)
            target   (dom/get-target event)
+
            editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
                         (= "rich-text" (obj/get target "className"))
                         (= "INPUT" (obj/get target "tagName"))
                         (= "TEXTAREA" (obj/get target "tagName")))]
-       (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?))))))
+       (st/emit! (kbd/->KeyboardEvent :up key shift? ctrl? alt? meta? mod? editing? event))))))
 
 (defn on-pointer-move [move-stream]
   (let [last-position (mf/use-var nil)]
@@ -353,16 +357,16 @@
 
          (rx/push! move-stream pt)
          (reset! last-position raw-pt)
-         (st/emit! (ms/->PointerEvent :delta delta
-                                      (kbd/ctrl? event)
-                                      (kbd/shift? event)
-                                      (kbd/alt? event)
-                                      (kbd/meta? event)))
-         (st/emit! (ms/->PointerEvent :viewport pt
-                                      (kbd/ctrl? event)
-                                      (kbd/shift? event)
-                                      (kbd/alt? event)
-                                      (kbd/meta? event))))))))
+         (st/emit! (mse/->PointerEvent :delta delta
+                                       (kbd/ctrl? event)
+                                       (kbd/shift? event)
+                                       (kbd/alt? event)
+                                       (kbd/meta? event)))
+         (st/emit! (mse/->PointerEvent :viewport pt
+                                       (kbd/ctrl? event)
+                                       (kbd/shift? event)
+                                       (kbd/alt? event)
+                                       (kbd/meta? event))))))))
 
 (defn on-mouse-wheel [zoom]
   (mf/use-callback
diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
index f90343331..6fd05d586 100644
--- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
@@ -15,8 +15,8 @@
    [app.main.data.workspace.colors :as dc]
    [app.main.refs :as refs]
    [app.main.store :as st]
-   [app.main.streams :as ms]
    [app.util.dom :as dom]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [cuerdas.core :as str]
    [rumext.v2 :as mf]))
@@ -154,14 +154,14 @@
      (mf/deps @moving-point from-p to-p width-p)
      (fn []
        (let [subs (->> st/stream
-                       (rx/filter ms/pointer-event?)
-                       (rx/filter #(= :viewport (:source %)))
-                       (rx/map :pt)
+                       (rx/filter mse/pointer-event?)
+                       (rx/filter #(= :viewport (mse/get-pointer-source %)))
+                       (rx/map mse/get-pointer-position)
                        (rx/subs
                         (fn [pt]
                           (case @moving-point
-                            :from-p (when on-change-start (on-change-start pt))
-                            :to-p (when on-change-finish (on-change-finish pt))
+                            :from-p  (when on-change-start (on-change-start pt))
+                            :to-p    (when on-change-finish (on-change-finish pt))
                             :width-p (when on-change-width
                                        (let [width-v (gpt/unit (gpt/to-vec from-p width-p))
                                              distance (gpt/point-line-distance pt from-p to-p)
diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
index 3f863cd25..1e6758d80 100644
--- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
@@ -31,6 +31,7 @@
    [app.util.debug :as dbg]
    [app.util.dom :as dom]
    [app.util.globals :as globals]
+   [app.util.keyboard :as kbd]
    [beicon.core :as rx]
    [goog.events :as events]
    [rumext.v2 :as mf])
@@ -99,14 +100,57 @@
        (when (not= @cursor new-cursor)
          (reset! cursor new-cursor))))))
 
-(defn setup-keyboard [alt? mod? space? z? shift?]
-  (hooks/use-stream ms/keyboard-alt #(reset! alt? %))
-  (hooks/use-stream ms/keyboard-mod #(do
-                                       (reset! mod? %)
-                                       (when-not % (reset! z? false)))) ;; In mac after command+z there is no event for the release of the z key
-  (hooks/use-stream ms/keyboard-space #(reset! space? %))
-  (hooks/use-stream ms/keyboard-z #(reset! z? %))
-  (hooks/use-stream ms/keyboard-shift #(reset! shift? %)))
+(defn setup-keyboard
+  [alt* mod* space* z* shift*]
+  (let [kbd-zoom-s
+        (mf/with-memo []
+          (->> ms/keyboard
+               (rx/filter kbd/key-down-event?)
+               (rx/filter kbd/mod-event?)
+               (rx/filter (fn [kevent]
+                            (or ^boolean (kbd/minus? kevent)
+                                ^boolean (kbd/underscore? kevent)
+                                ^boolean (kbd/equals? kevent)
+                                ^boolean (kbd/plus? kevent))))
+               (rx/dedupe)))
+
+        kbd-shift-s
+        (mf/with-memo []
+          (->> ms/keyboard
+               (rx/filter kbd/shift-key?)
+               (rx/filter (complement kbd/editing-event?))
+               (rx/map kbd/key-down-event?)
+               (rx/dedupe)))
+
+        kbd-z-s
+        (mf/with-memo []
+          (->> ms/keyboard
+               (rx/filter kbd/z?)
+               (rx/filter (complement kbd/editing-event?))
+               (rx/map kbd/key-down-event?)
+               (rx/dedupe)))]
+
+    (hooks/use-stream ms/keyboard-alt (partial reset! alt*))
+    (hooks/use-stream ms/keyboard-space (partial reset! space*))
+    (hooks/use-stream kbd-z-s (partial reset! z*))
+    (hooks/use-stream kbd-shift-s (partial reset! shift*))
+    (hooks/use-stream ms/keyboard-mod
+                      (fn [value]
+                        (reset! mod* value)
+                        ;; In mac after command+z there is no event
+                        ;; for the release of the z key
+                        (when-not ^boolean value
+                          (reset! z* false))))
+
+    (hooks/use-stream kbd-zoom-s
+                      (fn [kevent]
+                        (dom/prevent-default kevent)
+                        (st/emit!
+                         (if (or ^boolean (kbd/minus? kevent)
+                                 ^boolean (kbd/underscore? kevent))
+                           (dw/decrease-zoom)
+                           (dw/increase-zoom)))))))
+
 
 (defn group-empty-space?
   "Given a group `group-id` check if `hover-ids` contains any of its children. If it doesn't means
diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs
index 7d4ecf95a..5151b2f50 100644
--- a/frontend/src/app/util/keyboard.cljs
+++ b/frontend/src/app/util/keyboard.cljs
@@ -9,15 +9,44 @@
    [app.config :as cfg]
    [cuerdas.core :as str]))
 
+(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
+  Object
+  (preventDefault [_]
+    (.preventDefault native-event))
+
+  (stopPropagation [_]
+    (.stopPropagation native-event)))
+
+(defn keyboard-event?
+  [o]
+  (instance? KeyboardEvent o))
+
+(defn key-up-event?
+  [^KeyboardEvent event]
+  (= :up (.-type event)))
+
+(defn key-down-event?
+  [^KeyboardEvent event]
+  (= :down (.-type event)))
+
+(defn mod-event?
+  [^KeyboardEvent event]
+  (true? (.-mod event)))
+
+(defn editing-event?
+  [^KeyboardEvent event]
+  (true? (.-editing event)))
+
 (defn is-key?
   [^string key]
-  (fn [^js e]
+  (fn [^KeyboardEvent e]
     (= (.-key e) key)))
 
 (defn is-key-ignore-case?
   [^string key]
-  (fn [^js e]
-    (= (str/upper (.-key e)) (str/upper key))))
+  (let [key (str/upper key)]
+    (fn [^KeyboardEvent e]
+      (= (str/upper (.-key e)) key))))
 
 (defn ^boolean alt?
   [^js event]
@@ -45,6 +74,10 @@
 (def enter? (is-key? "Enter"))
 (def space? (is-key? " "))
 (def z? (is-key-ignore-case? "z"))
+(def equals? (is-key? "="))
+(def plus? (is-key? "+"))
+(def minus? (is-key? "-"))
+(def underscore? (is-key? "_"))
 (def up-arrow? (is-key? "ArrowUp"))
 (def down-arrow? (is-key? "ArrowDown"))
 (def left-arrow? (is-key? "ArrowLeft"))
@@ -58,6 +91,3 @@
 (def home? (is-key? "Home"))
 (def tab? (is-key? "Tab"))
 
-(defn editing? [e]
-  (.-editing ^js e))
-
diff --git a/frontend/src/app/util/mouse.cljs b/frontend/src/app/util/mouse.cljs
new file mode 100644
index 000000000..6102fecfa
--- /dev/null
+++ b/frontend/src/app/util/mouse.cljs
@@ -0,0 +1,63 @@
+;; 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) KALEIDOS INC
+
+(ns app.util.mouse)
+
+(defrecord MouseEvent [type ctrl shift alt meta])
+(defrecord PointerEvent [source pt ctrl shift alt meta])
+(defrecord ScrollEvent [point])
+
+(defn mouse-event?
+  [v]
+  (instance? MouseEvent v))
+
+(defn pointer-event?
+  [v]
+  (instance? PointerEvent v))
+
+(defn scroll-event?
+  [v]
+  (instance? ScrollEvent v))
+
+(defn mouse-down-event?
+  [^MouseEvent v]
+  (= :down (.-type v)))
+
+(defn mouse-up-event?
+  [^MouseEvent v]
+  (= :up (.-type v)))
+
+(defn mouse-click-event?
+  [^MouseEvent v]
+  (= :click (.-type v)))
+
+(defn mouse-double-click-event?
+  [^MouseEvent v]
+  (= :double-click (.-type v)))
+
+(defn get-pointer-source
+  [^PointerEvent ev]
+  (.-source ev))
+
+(defn get-pointer-position
+  [^PointerEvent ev]
+  (.-pt ev))
+
+(defn get-pointer-ctrl-mod
+  [^PointerEvent ev]
+  (.-ctrl ev))
+
+(defn get-pointer-meta-mod
+  [^PointerEvent ev]
+  (.-meta ev))
+
+(defn get-pointer-alt-mod
+  [^PointerEvent ev]
+  (.-meta ev))
+
+(defn get-pointer-shift-mod
+  [^PointerEvent ev]
+  (.-meta ev))