diff --git a/addons/website/static/src/snippets/s_image_gallery/options.js b/addons/website/static/src/snippets/s_image_gallery/options.js
index ff7526ea4e1fe2793a4be2ea8ec73a6c0f93262b..b0298cfc989022fde9b08763122b06101b4c79c8 100644
--- a/addons/website/static/src/snippets/s_image_gallery/options.js
+++ b/addons/website/static/src/snippets/s_image_gallery/options.js
@@ -16,6 +16,18 @@ options.registry.gallery = options.Class.extend({
      */
     start: function () {
         var self = this;
+        // TODO In master: define distinct classes.
+        // Differentiate both instances of this class: we want to avoid
+        // registering the same event listener twice.
+        this.hasAddImages = this.el.querySelector("we-button[data-add-images]");
+
+        if (!this.hasAddImages) {
+            const containerEl = this.$target[0].querySelector(":scope > .container, :scope > .container-fluid, :scope > .o_container_small");
+            if (containerEl.querySelector(":scope > *:not(div)")) {
+                self.mode(null, self.getMode());
+            }
+            return this._super.apply(this, arguments);
+        }
 
         // Make sure image previews are updated if images are changed
         this.$target.on('image_changed.gallery', 'img', function (ev) {
@@ -43,11 +55,6 @@ options.registry.gallery = options.Class.extend({
             }
         });
 
-        const $container = this.$('> .container, > .container-fluid, > .o_container_small');
-        if ($container.find('> *:not(div)').length) {
-            self.mode(null, self.getMode());
-        }
-
         return this._super.apply(this, arguments);
     },
     /**
@@ -302,12 +309,24 @@ options.registry.gallery = options.Class.extend({
      */
     notify: function (name, data) {
         this._super(...arguments);
+        // TODO Remove in master.
+        if (!this.hasAddImages) {
+            // In stable, the widget is instanciated twice. We do not want
+            // operations, especially moves, to be performed twice.
+            // We therefore ignore the requests from one of the instances.
+            return;
+        }
         if (name === 'image_removed') {
             data.$image.remove(); // Force the removal of the image before reset
             this.mode('reset', this.getMode());
         } else if (name === 'image_index_request') {
             var imgs = this._getImages();
             var position = _.indexOf(imgs, data.$image[0]);
+            if (position === 0 && data.position === "prev") {
+                data.position = "last";
+            } else if (position === imgs.length - 1 && data.position === "next") {
+                data.position = "first";
+            }
             imgs.splice(position, 1);
             switch (data.position) {
                 case 'first':
diff --git a/addons/website/static/tests/tours/snippet_images_wall.js b/addons/website/static/tests/tours/snippet_images_wall.js
new file mode 100644
index 0000000000000000000000000000000000000000..4234101179079553c5e38d357b04ef48dbd22607
--- /dev/null
+++ b/addons/website/static/tests/tours/snippet_images_wall.js
@@ -0,0 +1,44 @@
+/** @odoo-module **/
+
+import tour from "web_tour.tour";
+import wTourUtils from "website.tour_utils";
+
+tour.register("snippet_images_wall", {
+    test: true,
+    url: "/",
+}, [
+    ...wTourUtils.clickOnEditAndWaitEditMode(),
+    wTourUtils.dragNDrop({
+        id: "s_images_wall",
+        name: "Images Wall",
+}), wTourUtils.clickOnSnippet({
+    id: "s_image_gallery",
+    name: "Images Wall",
+}), {
+    // Prefixing selectors with #wrap to avoid matching droppable block.
+    content: "Click on third image",
+    trigger: "#wrap .s_image_gallery img[data-index='2']",
+}, {
+    content: "Click on move to previous",
+    trigger: ".snippet-option-gallery_img we-button[data-position='prev']",
+}, {
+    content: "Click on move to first",
+    extra_trigger: "#wrap .s_image_gallery .o_masonry_col:nth-child(2):has(img[data-index='1'][data-original-src*='sign'])",
+    trigger: ".snippet-option-gallery_img we-button[data-position='first']",
+}, {
+    content: "Click on move to previous",
+    extra_trigger: "#wrap .s_image_gallery .o_masonry_col:nth-child(1):has(img[data-index='0'][data-original-src*='sign'])",
+    trigger: ".snippet-option-gallery_img we-button[data-position='prev']",
+}, {
+    content: "Click on move to next",
+    extra_trigger: "#wrap .s_image_gallery .o_masonry_col:nth-child(3):has(img[data-index='5'][data-original-src*='sign'])",
+    trigger: ".snippet-option-gallery_img we-button[data-position='next']",
+}, {
+    content: "Click on move to last",
+    extra_trigger: "#wrap .s_image_gallery .o_masonry_col:nth-child(1):has(img[data-index='0'][data-original-src*='sign'])",
+    trigger: ".snippet-option-gallery_img we-button[data-position='last']",
+}, {
+    content: "Check layout",
+    trigger: "#wrap .s_image_gallery .o_masonry_col:nth-child(3):has(img[data-index='5'][data-original-src*='sign'])",
+    run: () => {}, // This is a check.
+}]);
diff --git a/addons/website/tests/test_snippets.py b/addons/website/tests/test_snippets.py
index a4b5bb380596a64859f5ebd379ca83259a772609..ad650f7a258d572581cd5ad0e476edac8b2f6b8c 100644
--- a/addons/website/tests/test_snippets.py
+++ b/addons/website/tests/test_snippets.py
@@ -53,3 +53,6 @@ class TestSnippets(odoo.tests.HttpCase):
             'url': base + '/web/image/website.s_banner_default_image.jpg',
         })
         self.start_tour("/", "snippet_image_gallery", login='admin')
+
+    def test_07_snippet_images_wall(self):
+        self.start_tour('/', 'snippet_images_wall', login='admin')