From b10a65666a521a863440ecc79349f3bbe5bab5b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <jeremie@baserow.io>
Date: Thu, 28 Nov 2024 08:47:54 +0000
Subject: [PATCH] Resolve "Create a multi-page container element"

---
 .../contrib/builder/api/data_sources/views.py |    4 +-
 .../builder/api/domains/serializers.py        |    4 +-
 .../contrib/builder/api/elements/views.py     |    1 +
 .../contrib/builder/api/serializers.py        |    2 +-
 .../contrib/builder/application_types.py      |    7 +-
 backend/src/baserow/contrib/builder/apps.py   |    4 +
 .../contrib/builder/elements/element_types.py |   57 +-
 .../contrib/builder/elements/mixins.py        |  127 +-
 .../contrib/builder/elements/models.py        |   35 +
 .../contrib/builder/elements/registries.py    |   59 +-
 .../0042_footerelement_headerelement.py       |   79 +
 .../baserow/contrib/builder/pages/handler.py  |   30 +-
 .../baserow/contrib/builder/pages/models.py   |   13 +
 .../baserow/contrib/builder/pages/service.py  |    6 +-
 .../data_sources/test_data_source_views.py    |    4 +-
 .../api/domains/test_domain_public_views.py   |   14 +-
 .../builder/api/pages/test_page_views.py      |    6 +-
 .../api/test_builder_application_views.py     |    4 +-
 .../builder/api/test_builder_serializer.py    |    2 +-
 .../test_workflow_actions_views.py            |    2 +-
 .../data_sources/test_data_source_handler.py  |    2 +-
 .../builder/domains/test_domain_handler.py    |    5 +-
 .../builder/elements/test_element_handler.py  |   33 +
 .../builder/elements/test_element_service.py  |    5 +
 .../builder/elements/test_element_types.py    |   12 +-
 .../test_header_footer_element_type.py        |   93 ++
 .../test_record_selector_element_type.py      |    7 +-
 .../builder/pages/test_page_handler.py        |    8 +-
 .../builder/test_builder_application_type.py  |   21 +-
 .../test_formula_property_extractor.py        |    2 +-
 .../builder/test_permissions_manager.py       |    4 +-
 .../test_upsert_row_service_type.py           |    2 +-
 .../integrations/local_baserow/test_mixins.py |    2 +-
 ...ultipage_header_and_footer_containers.json |    7 +
 e2e-tests/tests/builder/builderPage.spec.ts   |    2 +-
 .../local_baserow/test_user_source_types.py   |    2 +-
 .../components/elements/AuthFormElement.vue   |    4 +-
 .../modules/builder/applicationTypes.js       |    9 +-
 .../ApplicationBuilderFormulaInput.vue        |   10 +-
 .../dataSource/DataSourceCreateEditModal.vue  |   25 +-
 .../dataSource/DataSourceDropdown.vue         |   50 +-
 .../components/elements/AddElementCard.vue    |   18 +-
 .../components/elements/AddElementModal.vue   |  115 +-
 .../components/elements/AddElementZone.vue    |    9 +-
 .../components/elements/ElementMenu.vue       |   89 +-
 .../components/elements/ElementPreview.vue    |  136 +-
 .../components/elements/ElementsList.vue      |    6 +-
 .../components/elements/ElementsListItem.vue  |   38 +-
 .../elements/baseComponents/ABLink.vue        |    4 +-
 .../elements/baseComponents/ABTable.vue       |    9 +-
 .../components/CollectionElementHeader.vue    |    4 +-
 .../elements/components/ColumnElement.vue     |   31 +-
 .../components/FormContainerElement.vue       |   33 +-
 .../components/MultiPageContainerElement.vue  |   82 +
 .../elements/components/RepeatElement.vue     |   33 +-
 .../collectionField/ButtonField.vue           |    2 +-
 .../components/forms/VisibilityForm.vue       |   14 +-
 .../forms/general/ChoiceElementForm.vue       |    2 +-
 .../general/MultiPageContainerElementForm.vue |  131 ++
 .../general/RecordSelectorElementForm.vue     |   18 +-
 .../forms/general/RepeatElementForm.vue       |    7 +-
 .../forms/general/TableElementForm.vue        |    7 +-
 .../general/settings/PropertyOptionForm.vue   |    1 -
 .../builder/components/event/Event.vue        |    8 +-
 .../components/page/CreatePageModal.vue       |    2 +-
 .../builder/components/page/PageContent.vue   |   43 +-
 .../builder/components/page/PageElement.vue   |   32 +-
 .../builder/components/page/PagePreview.vue   |  340 +++--
 .../builder/components/page/PageTemplate.vue  |    8 +-
 .../components/page/PageTemplateContent.vue   |   21 +-
 .../page/UserSourceUsersContext.vue           |    2 +-
 .../page/header/ElementsContext.vue           |  195 ++-
 .../components/page/header/PageActions.vue    |    8 +-
 .../components/page/header/PageHeader.vue     |    6 -
 .../page/header/PageHeaderMenuItems.vue       |    4 +-
 .../components/page/settings/PageSettings.vue |   12 +-
 .../page/settings/PageSettingsForm.vue        |    7 +-
 .../page/settings/PageVisibilityForm.vue      |   14 +-
 .../page/settings/PageVisibilitySettings.vue  |    6 +-
 .../page/sidePanels/EventsSidePanel.vue       |    7 +-
 .../page/sidePanels/GeneralSidePanel.vue      |    5 -
 .../components/settings/GeneralSettings.vue   |    1 -
 .../settings/UserSourcesSettings.vue          |    1 -
 .../RefreshDataSourceWorkflowActionForm.vue   |   52 +-
 .../modules/builder/elementTypeMixins.js      |  364 +++++
 web-frontend/modules/builder/elementTypes.js  | 1323 ++++++++---------
 web-frontend/modules/builder/enums.js         |   14 +-
 web-frontend/modules/builder/locales/en.json  |   36 +-
 .../builder/mixins/collectionElement.js       |   13 +-
 .../builder/mixins/collectionElementForm.js   |   41 +-
 .../modules/builder/mixins/collectionField.js |    2 +-
 .../builder/mixins/containerElement.js        |   17 +-
 .../modules/builder/mixins/element.js         |    8 +-
 .../modules/builder/mixins/elementForm.js     |    2 +-
 .../builder/mixins/elementSidePanel.js        |   25 +-
 .../modules/builder/mixins/formElement.js     |   12 +-
 .../modules/builder/mixins/formElementForm.js |    2 +-
 .../modules/builder/mixins/visibilityForm.js  |    3 +-
 .../modules/builder/pages/pageEditor.vue      |   33 +-
 .../modules/builder/pages/publicPage.vue      |   52 +-
 web-frontend/modules/builder/plugin.js        |    4 +
 web-frontend/modules/builder/store/element.js |   35 +-
 .../modules/builder/store/elementContent.js   |    4 +-
 web-frontend/modules/builder/store/page.js    |    5 +
 .../modules/builder/workflowActionTypes.js    |    1 -
 .../components/builder/add_element_zone.scss  |   51 +-
 .../assets/scss/components/builder/all.scss   |    1 +
 .../components/builder/element_preview.scss   |   16 +-
 .../elements/ab_components/ab_table.scss      |    2 +-
 .../builder/elements/forms/all.scss           |    1 +
 .../multi_page_container_element_form.scss    |   14 +
 .../elements/forms/visibility_form.scss       |   25 +-
 .../components/builder/elements_context.scss  |   30 +
 .../components/builder/elements_list.scss     |  158 +-
 .../builder/elements_list_item.scss           |   90 ++
 .../scss/components/builder/page_preview.scss |   38 +
 .../workflowActions/WorkflowAction.vue        |    4 +-
 .../mixins/localBaserowService.js             |   10 -
 .../elements/components/ChoiceElement.spec.js |    3 +-
 .../components/DateTimePickerElement.spec.js  |    3 +-
 .../components/HeadingElement.spec.js         |    8 +-
 .../components/RecordSelectorElement.spec.js  |    6 +-
 .../__snapshots__/HeadingElement.spec.js.snap |    2 +-
 .../test/unit/builder/elementTypes.spec.js    |  417 +++++-
 124 files changed, 3474 insertions(+), 1729 deletions(-)
 create mode 100644 backend/src/baserow/contrib/builder/migrations/0042_footerelement_headerelement.py
 create mode 100644 backend/tests/baserow/contrib/builder/elements/test_header_footer_element_type.py
 create mode 100644 changelog/entries/unreleased/feature/2486_builder_add_the_multipage_header_and_footer_containers.json
 create mode 100644 web-frontend/modules/builder/components/elements/components/MultiPageContainerElement.vue
 create mode 100644 web-frontend/modules/builder/components/elements/components/forms/general/MultiPageContainerElementForm.vue
 create mode 100644 web-frontend/modules/builder/elementTypeMixins.js
 create mode 100644 web-frontend/modules/core/assets/scss/components/builder/elements/forms/multi_page_container_element_form.scss
 create mode 100644 web-frontend/modules/core/assets/scss/components/builder/elements_list_item.scss

diff --git a/backend/src/baserow/contrib/builder/api/data_sources/views.py b/backend/src/baserow/contrib/builder/api/data_sources/views.py
index 8269b8a92..975307f1d 100644
--- a/backend/src/baserow/contrib/builder/api/data_sources/views.py
+++ b/backend/src/baserow/contrib/builder/api/data_sources/views.py
@@ -254,7 +254,9 @@ class DataSourceView(APIView):
         if "page_id" in request.data:
             page = PageHandler().get_page(
                 int(request.data["page_id"]),
-                base_queryset=Page.objects.filter(builder=data_source.page.builder),
+                base_queryset=Page.objects_with_shared.filter(
+                    builder=data_source.page.builder
+                ),
             )
 
         # Do we have a service?
diff --git a/backend/src/baserow/contrib/builder/api/domains/serializers.py b/backend/src/baserow/contrib/builder/api/domains/serializers.py
index 7cdfae260..0c09df93a 100644
--- a/backend/src/baserow/contrib/builder/api/domains/serializers.py
+++ b/backend/src/baserow/contrib/builder/api/domains/serializers.py
@@ -24,6 +24,7 @@ from baserow.contrib.builder.domains.registries import domain_type_registry
 from baserow.contrib.builder.elements.models import Element
 from baserow.contrib.builder.elements.registries import element_type_registry
 from baserow.contrib.builder.models import Builder
+from baserow.contrib.builder.pages.handler import PageHandler
 from baserow.contrib.builder.pages.models import Page
 from baserow.core.services.registries import service_type_registry
 from baserow.core.user_sources.models import UserSource
@@ -112,6 +113,7 @@ class PublicElementSerializer(serializers.ModelSerializer):
             "page_id",
             "type",
             "order",
+            "page_id",
             "parent_element_id",
             "place_in_container",
             "visibility",
@@ -272,7 +274,7 @@ class PublicBuilderSerializer(serializers.ModelSerializer):
         :return: A list of serialized pages that belong to this instance.
         """
 
-        pages = instance.page_set.all()
+        pages = PageHandler().get_pages(instance)
 
         return PublicPageSerializer(pages, many=True).data
 
diff --git a/backend/src/baserow/contrib/builder/api/elements/views.py b/backend/src/baserow/contrib/builder/api/elements/views.py
index 2fe28479f..630c6b90e 100644
--- a/backend/src/baserow/contrib/builder/api/elements/views.py
+++ b/backend/src/baserow/contrib/builder/api/elements/views.py
@@ -133,6 +133,7 @@ class ElementsView(APIView):
     @map_exceptions(
         {
             PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
+            ElementNotInSamePage: ERROR_ELEMENT_NOT_IN_SAME_PAGE,
         }
     )
     @validate_body_custom_fields(
diff --git a/backend/src/baserow/contrib/builder/api/serializers.py b/backend/src/baserow/contrib/builder/api/serializers.py
index 4e7dcb393..7189b1e34 100644
--- a/backend/src/baserow/contrib/builder/api/serializers.py
+++ b/backend/src/baserow/contrib/builder/api/serializers.py
@@ -48,7 +48,7 @@ class BuilderSerializer(serializers.ModelSerializer):
         :return: A list of serialized pages that belong to this instance.
         """
 
-        pages = instance.page_set.all()
+        pages = PageHandler().get_pages(instance)
 
         user = self.context.get("user")
         request = self.context.get("request")
diff --git a/backend/src/baserow/contrib/builder/application_types.py b/backend/src/baserow/contrib/builder/application_types.py
index 0732a5df8..ae9793f44 100755
--- a/backend/src/baserow/contrib/builder/application_types.py
+++ b/backend/src/baserow/contrib/builder/application_types.py
@@ -167,7 +167,12 @@ class BuilderApplicationType(ApplicationType):
             for us in UserSourceHandler().get_user_sources(builder)
         ]
 
-        pages = builder.page_set.all().prefetch_related("element_set", "datasource_set")
+        pages = PageHandler().get_pages(
+            builder,
+            base_queryset=Page.objects_with_shared.prefetch_related(
+                "element_set", "datasource_set"
+            ),
+        )
 
         serialized_pages = [
             PageHandler().export_page(
diff --git a/backend/src/baserow/contrib/builder/apps.py b/backend/src/baserow/contrib/builder/apps.py
index d314521d9..0d2c695b2 100644
--- a/backend/src/baserow/contrib/builder/apps.py
+++ b/backend/src/baserow/contrib/builder/apps.py
@@ -175,7 +175,9 @@ class BuilderConfig(AppConfig):
             ChoiceElementType,
             ColumnElementType,
             DateTimePickerElementType,
+            FooterElementType,
             FormContainerElementType,
+            HeaderElementType,
             HeadingElementType,
             IFrameElementType,
             ImageElementType,
@@ -203,6 +205,8 @@ class BuilderConfig(AppConfig):
         element_type_registry.register(CheckboxElementType())
         element_type_registry.register(IFrameElementType())
         element_type_registry.register(DateTimePickerElementType())
+        element_type_registry.register(HeaderElementType())
+        element_type_registry.register(FooterElementType())
 
         from .domains.domain_types import CustomDomainType, SubDomainType
         from .domains.registries import domain_type_registry
diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py
index cfae85581..c7679df2f 100644
--- a/backend/src/baserow/contrib/builder/elements/element_types.py
+++ b/backend/src/baserow/contrib/builder/elements/element_types.py
@@ -33,6 +33,7 @@ from baserow.contrib.builder.elements.mixins import (
     CollectionElementWithFieldsTypeMixin,
     ContainerElementTypeMixin,
     FormElementTypeMixin,
+    MultiPageElementTypeMixin,
 )
 from baserow.contrib.builder.elements.models import (
     INPUT_TEXT_TYPES,
@@ -43,7 +44,9 @@ from baserow.contrib.builder.elements.models import (
     ColumnElement,
     DateTimePickerElement,
     Element,
+    FooterElement,
     FormContainerElement,
+    HeaderElement,
     HeadingElement,
     IFrameElement,
     ImageElement,
@@ -117,7 +120,7 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
     type = "column"
     model_class = ColumnElement
 
-    class SerializedDict(ElementDict):
+    class SerializedDict(ContainerElementTypeMixin.SerializedDict):
         column_amount: int
         column_gap: int
         alignment: str
@@ -191,8 +194,8 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
         """
 
         return [
-            element_type.type
-            for element_type in element_type_registry.get_all()
+            element_type
+            for element_type in super().child_types_allowed
             if element_type.type != self.type
         ]
 
@@ -210,7 +213,7 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
     ]
     simple_formula_fields = ["submit_button_label"]
 
-    class SerializedDict(ElementDict):
+    class SerializedDict(ContainerElementTypeMixin.SerializedDict):
         submit_button_label: BaserowFormula
         reset_initial_values_post_submission: bool
 
@@ -261,8 +264,8 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
         """
 
         return [
-            element_type.type
-            for element_type in element_type_registry.get_all()
+            element_type
+            for element_type in super().child_types_allowed
             if element_type.type != self.type
         ]
 
@@ -858,6 +861,16 @@ class NavigationElementManager:
             "target": "blank",
         }
 
+    def validate_place(
+        self,
+        page: Page,
+        parent_element: Optional[Element],
+        place_in_container: str,
+    ):
+        """
+        We need it because it's called in the prepare_value_for_db.
+        """
+
     def prepare_value_for_db(
         self, values: Dict, instance: Optional[LinkElement] = None
     ):
@@ -1939,3 +1952,35 @@ class DateTimePickerElementType(FormElementTypeMixin, ElementType):
             "include_time": False,
             "time_format": DATE_TIME_FORMAT_CHOICES[0][0],
         }
+
+
+class MultiPageContainerElementType(
+    ContainerElementTypeMixin, MultiPageElementTypeMixin, ElementType
+):
+    """
+    A base class container element that can be displayed on multiple pages.
+    """
+
+    class SerializedDict(
+        MultiPageElementTypeMixin.SerializedDict,
+        ContainerElementTypeMixin.SerializedDict,
+    ):
+        ...
+
+
+class HeaderElementType(MultiPageContainerElementType):
+    """
+    A container element that can be displayed on multiple pages.
+    """
+
+    type = "header"
+    model_class = HeaderElement
+
+
+class FooterElementType(MultiPageContainerElementType):
+    """
+    A container element that can be displayed on multiple pages.
+    """
+
+    type = "footer"
+    model_class = FooterElement
diff --git a/backend/src/baserow/contrib/builder/elements/mixins.py b/backend/src/baserow/contrib/builder/elements/mixins.py
index ca7dcd090..5542f690f 100644
--- a/backend/src/baserow/contrib/builder/elements/mixins.py
+++ b/backend/src/baserow/contrib/builder/elements/mixins.py
@@ -40,6 +40,7 @@ from baserow.contrib.builder.elements.types import (
     ElementSubClass,
 )
 from baserow.contrib.builder.formula_importer import import_formula
+from baserow.contrib.builder.pages.handler import PageHandler
 from baserow.contrib.builder.types import ElementDict
 from baserow.contrib.database.fields.utils import get_field_id_from_field_key
 from baserow.core.formula.types import BaserowFormula
@@ -59,10 +60,14 @@ class ContainerElementTypeMixin:
         """
         Lets you define which children types can be placed inside the container.
 
-        :return: All the allowed children types
+        By default, multi-page elements are not allowed inside any container.
         """
 
-        return [element_type.type for element_type in element_type_registry.get_all()]
+        return [
+            element_type
+            for element_type in element_type_registry.get_all()
+            if not element_type.is_multi_page_element
+        ]
 
     def get_new_place_in_container(
         self, container_element: ContainerElement, places_removed: List[str]
@@ -128,6 +133,8 @@ class ContainerElementTypeMixin:
         :raises DRFValidationError: If the place in container is invalid
         """
 
+        return True
+
 
 class CollectionElementTypeMixin:
     is_collection_element = True
@@ -738,3 +745,119 @@ class FormElementTypeMixin:
             )
 
         return value
+
+
+class MultiPageElementTypeMixin:
+    is_multi_page_element = True
+
+    @property
+    def serializer_field_names(self):
+        return super().serializer_field_names + [
+            "share_type",
+            "pages",
+        ]
+
+    @property
+    def allowed_fields(self):
+        return super().allowed_fields + [
+            "share_type",
+        ]
+
+    class SerializedDict(ElementDict):
+        share_type: str
+        pages: List[int]
+
+    def after_create(self, instance, values):
+        """
+        Add the pages
+        """
+
+        from baserow.contrib.builder.pages.models import Page
+
+        super().after_create(instance, values)
+
+        if "pages" in values:
+            pages = PageHandler().get_pages(
+                instance.page.builder,
+                base_queryset=Page.objects.filter(
+                    id__in=[p.id for p in values["pages"]]
+                ),
+            )
+            instance.pages.add(*pages)
+
+    def after_update(self, instance: Any, values: Dict, changes: Dict[str, Tuple]):
+        """
+        Updates the pages.
+        """
+
+        from baserow.contrib.builder.pages.models import Page
+
+        super().after_update(instance, values, changes)
+
+        if "pages" in values:
+            pages = PageHandler().get_pages(
+                instance.page.builder,
+                base_queryset=Page.objects.filter(
+                    id__in=[p.id for p in values["pages"]]
+                ),
+            )
+            instance.pages.clear()
+            instance.pages.add(*pages)
+
+    def serialize_property(
+        self,
+        element: "MultiPageElementTypeMixin",
+        prop_name: str,
+        files_zip=None,
+        storage=None,
+        cache=None,
+        **kwargs,
+    ):
+        """
+        You can customize the behavior of the serialization of a property with this
+        hook.
+        """
+
+        if prop_name == "pages":
+            return [page.id for page in element.pages.all()]
+
+        return super().serialize_property(
+            element,
+            prop_name,
+            files_zip=files_zip,
+            storage=storage,
+            cache=cache,
+            **kwargs,
+        )
+
+    def create_instance_from_serialized(
+        self,
+        serialized_values: Dict[str, Any],
+        id_mapping,
+        files_zip=None,
+        storage=None,
+        cache=None,
+        **kwargs,
+    ):
+        """Deals with the fields"""
+
+        pages = serialized_values.pop("pages", [])
+
+        instance = super().create_instance_from_serialized(
+            serialized_values,
+            id_mapping,
+            files_zip=files_zip,
+            storage=storage,
+            cache=cache,
+            **kwargs,
+        )
+
+        pages = [id_mapping["builder_pages"][page_id] for page_id in pages]
+
+        if pages:
+            instance.pages.add(*pages)
+
+        return instance
+
+    def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
+        return {"share_type": "all"}
diff --git a/backend/src/baserow/contrib/builder/elements/models.py b/backend/src/baserow/contrib/builder/elements/models.py
index 3e78c7e1d..75435893e 100644
--- a/backend/src/baserow/contrib/builder/elements/models.py
+++ b/backend/src/baserow/contrib/builder/elements/models.py
@@ -906,3 +906,38 @@ class DateTimePickerElement(FormElement):
         max_length=32,
         help_text="24 (14:00) or 12 (02:30) PM",
     )
+
+
+class MultiPageElement(Element):
+    """
+    A container element that can contain other elements and be can shared across
+    multiple pages.
+    """
+
+    class SHARE_TYPE(models.TextChoices):
+        ALL = "all"
+        ONLY = "only"
+        EXCEPT = "except"
+
+    share_type = models.CharField(
+        choices=SHARE_TYPE.choices,
+        max_length=10,
+        default=SHARE_TYPE.ALL,
+    )
+
+    pages = models.ManyToManyField("builder.Page", blank=True)
+
+    class Meta:
+        abstract = True
+
+
+class HeaderElement(MultiPageElement, ContainerElement):
+    """
+    A multi-page container element positioned at the top of the page.
+    """
+
+
+class FooterElement(MultiPageElement, ContainerElement):
+    """
+    A multi-page container element positioned at the bottom of the page.
+    """
diff --git a/backend/src/baserow/contrib/builder/elements/registries.py b/backend/src/baserow/contrib/builder/elements/registries.py
index 8c5d8a236..2b09989ee 100644
--- a/backend/src/baserow/contrib/builder/elements/registries.py
+++ b/backend/src/baserow/contrib/builder/elements/registries.py
@@ -21,6 +21,7 @@ from rest_framework.exceptions import ValidationError
 
 from baserow.contrib.builder.formula_importer import import_formula
 from baserow.contrib.builder.mixins import BuilderInstanceWithFormulaMixin
+from baserow.contrib.builder.pages.models import Page
 from baserow.contrib.database.db.functions import RandomUUID
 from baserow.core.registry import (
     CustomFieldsInstanceMixin,
@@ -58,6 +59,9 @@ class ElementType(
     parent_property_name = "page"
     id_mapping_name = BUILDER_PAGE_ELEMENTS
 
+    # Whether this element is a multi-page element and should be placed on shared page.
+    is_multi_page_element = False
+
     # The order in which this element type is imported in `import_elements`.
     # By default, the priority is `0`, the lowest value. If this property is
     # not overridden, then the instance is imported last.
@@ -80,25 +84,62 @@ class ElementType(
         parent_element_id = values.get(
             "parent_element_id", getattr(instance, "parent_element_id", None)
         )
-        place_in_container = values.get("place_in_container", None)
 
+        if instance:
+            place_in_container = values.get(
+                "place_in_container", instance.place_in_container
+            )
+            page = values.get("page", instance.page)
+        else:
+            place_in_container = values.get("place_in_container", None)
+            page = values["page"]
+
+        parent_element = None
         if parent_element_id is not None:
             parent_element = ElementHandler().get_element(parent_element_id)
-            parent_element_type = element_type_registry.get_by_model(parent_element)
 
-            if self.type not in parent_element_type.child_types_allowed:
+        # Validate the place for this element
+        self.validate_place(page, parent_element, place_in_container)
+
+        return values
+
+    def validate_place(
+        self,
+        page: Page,
+        parent_element: Optional[ElementSubClass],
+        place_in_container: str,
+    ):
+        """
+        Validates the page/parent_element/place_in_container for this element.
+        Can be overridden to change the behaviour.
+
+        :param page: the page we want to add/move the element to.
+        :param parent_element: the parent_element if any.
+        :param place_in_container: the place in container in the parent.
+        :raises ValidationError: if the the element place is not allowed.
+        """
+
+        if parent_element:
+            if self.type not in [
+                e.type for e in parent_element.get_type().child_types_allowed
+            ]:
                 raise ValidationError(
-                    f"Container of type {parent_element_type.type} can't have child of "
+                    f"Container of type {parent_element.get_type().type} can't have child of "
                     f"type {self.type}"
                 )
 
-            if place_in_container is not None:
-                parent_element_type.validate_place_in_container(
-                    place_in_container, parent_element
+            # If we have a parent, we validate the place is accepted by this container.
+            parent_element.get_type().validate_place_in_container(
+                place_in_container, parent_element
+            )
+        else:
+            if self.is_multi_page_element != page.shared:
+                raise ValidationError(
+                    "This element type can't be added as root of a "
+                    f"{'an unshared' if self.is_multi_page_element else 'the shared'} "
+                    "page."
                 )
 
-        return values
-
     def after_create(self, instance: ElementSubClass, values: Dict):
         """
         This hook is called right after the element has been created.
diff --git a/backend/src/baserow/contrib/builder/migrations/0042_footerelement_headerelement.py b/backend/src/baserow/contrib/builder/migrations/0042_footerelement_headerelement.py
new file mode 100644
index 000000000..5ee6b1d5f
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/migrations/0042_footerelement_headerelement.py
@@ -0,0 +1,79 @@
+# Generated by Django 5.0.9 on 2024-10-17 08:01
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("builder", "0041_builder_login_page_page_role_type_page_roles_and_more"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="FooterElement",
+            fields=[
+                (
+                    "element_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="builder.element",
+                    ),
+                ),
+                (
+                    "share_type",
+                    models.CharField(
+                        choices=[
+                            ("all", "All"),
+                            ("only", "Only"),
+                            ("except", "Except"),
+                        ],
+                        default="all",
+                        max_length=10,
+                    ),
+                ),
+                ("pages", models.ManyToManyField(blank=True, to="builder.page")),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("builder.element",),
+        ),
+        migrations.CreateModel(
+            name="HeaderElement",
+            fields=[
+                (
+                    "element_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="builder.element",
+                    ),
+                ),
+                (
+                    "share_type",
+                    models.CharField(
+                        choices=[
+                            ("all", "All"),
+                            ("only", "Only"),
+                            ("except", "Except"),
+                        ],
+                        default="all",
+                        max_length=10,
+                    ),
+                ),
+                ("pages", models.ManyToManyField(blank=True, to="builder.page")),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("builder.element",),
+        ),
+    ]
diff --git a/backend/src/baserow/contrib/builder/pages/handler.py b/backend/src/baserow/contrib/builder/pages/handler.py
index 34da0e525..c6d51ccb5 100644
--- a/backend/src/baserow/contrib/builder/pages/handler.py
+++ b/backend/src/baserow/contrib/builder/pages/handler.py
@@ -51,20 +51,34 @@ class PageHandler:
         """
 
         if base_queryset is None:
-            base_queryset = Page.objects
+            base_queryset = Page.objects_with_shared
 
         try:
-            return base_queryset.select_related("builder", "builder__workspace").get(
-                id=page_id
-            )
+            return base_queryset.select_related("builder__workspace").get(id=page_id)
         except Page.DoesNotExist:
             raise PageDoesNotExist()
 
     def get_shared_page(self, builder: Builder) -> Page:
-        return Page.objects.select_related("builder", "builder__workspace").get(
+        """
+        Returns the shared page for the given builder.
+        """
+
+        return Page.objects_with_shared.select_related("builder__workspace").get(
             builder=builder, shared=True
         )
 
+    def get_pages(self, builder, base_queryset: Optional[QuerySet] = None):
+        """
+        Returns all the page in the current builder.
+        """
+
+        if base_queryset is None:
+            base_queryset = Page.objects_with_shared.all()
+
+        return base_queryset.filter(builder=builder).select_related(
+            "builder__workspace"
+        )
+
     def create_shared_page(self, builder: Builder) -> Page:
         """
         Creates the shared page of the given builder.
@@ -153,7 +167,7 @@ class PageHandler:
             self.is_page_path_unique(
                 page.builder,
                 path,
-                base_queryset=Page.objects.exclude(
+                base_queryset=Page.objects_with_shared.exclude(
                     id=page.id
                 ),  # We don't want to conflict with the current page
                 raises=True,
@@ -188,7 +202,7 @@ class PageHandler:
         """
 
         if base_qs is None:
-            base_qs = Page.objects.filter(builder=builder, shared=False)
+            base_qs = Page.objects.filter(builder=builder)
 
         try:
             full_order = Page.order_objects(base_qs, order)
@@ -345,7 +359,7 @@ class PageHandler:
         :return: If the path is unique
         """
 
-        queryset = Page.objects if base_queryset is None else base_queryset
+        queryset = Page.objects_with_shared if base_queryset is None else base_queryset
 
         existing_paths = queryset.filter(builder=builder).values_list("path", flat=True)
 
diff --git a/backend/src/baserow/contrib/builder/pages/models.py b/backend/src/baserow/contrib/builder/pages/models.py
index 759563a6b..8c8cf2d7c 100644
--- a/backend/src/baserow/contrib/builder/pages/models.py
+++ b/backend/src/baserow/contrib/builder/pages/models.py
@@ -20,6 +20,16 @@ if typing.TYPE_CHECKING:
     from baserow.contrib.builder.models import Builder
 
 
+class PageWithoutSharedManager(models.Manager):
+    """
+    Manager for the Page model.
+    Excludes by default the shared page.
+    """
+
+    def get_queryset(self):
+        return super().get_queryset().filter(shared=False)
+
+
 class Page(
     HierarchicalModelMixin,
     TrashableModelMixin,
@@ -36,6 +46,9 @@ class Page(
         ALLOW_ALL_EXCEPT = "allow_all_except"
         DISALLOW_ALL_EXCEPT = "disallow_all_except"
 
+    objects = PageWithoutSharedManager()
+    objects_with_shared = models.Manager()
+
     builder = models.ForeignKey("builder.Builder", on_delete=models.CASCADE)
     order = models.PositiveIntegerField()
     name = models.CharField(max_length=255)
diff --git a/backend/src/baserow/contrib/builder/pages/service.py b/backend/src/baserow/contrib/builder/pages/service.py
index 272c6196b..f8c54d964 100644
--- a/backend/src/baserow/contrib/builder/pages/service.py
+++ b/backend/src/baserow/contrib/builder/pages/service.py
@@ -37,8 +37,7 @@ class PageService:
         :return: The model instance of the Page
         """
 
-        base_queryset = Page.objects.select_related("builder", "builder__workspace")
-        page = self.handler.get_page(page_id, base_queryset=base_queryset)
+        page = self.handler.get_page(page_id)
 
         CoreHandler().check_permissions(
             user,
@@ -148,7 +147,8 @@ class PageService:
             context=builder,
         )
 
-        all_pages = Page.objects.filter(builder_id=builder.id, shared=False)
+        all_pages = self.handler.get_pages(builder, base_queryset=Page.objects)
+
         user_pages = CoreHandler().filter_queryset(
             user,
             OrderPagesBuilderOperationType.type,
diff --git a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
index 227d365c4..b173498c8 100644
--- a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
+++ b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
@@ -1620,7 +1620,7 @@ def test_dispatch_data_sources_with_formula_using_datasource_calling_a_shared_da
     integration = data_fixture.create_local_baserow_integration(
         user=user, application=builder
     )
-    shared_page = builder.page_set.first()
+    shared_page = builder.shared_page
     page = data_fixture.create_builder_page(user=user, builder=builder)
 
     data_source2 = data_fixture.create_builder_local_baserow_get_row_data_source(
@@ -1686,7 +1686,7 @@ def test_dispatch_only_shared_data_sources(data_fixture, api_client):
     integration = data_fixture.create_local_baserow_integration(
         user=user, application=builder
     )
-    shared_page = builder.page_set.first()
+    shared_page = builder.shared_page
     page = data_fixture.create_builder_page(user=user, builder=builder)
 
     shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
diff --git a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
index 27728e347..35d4b168a 100644
--- a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
+++ b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
@@ -129,9 +129,12 @@ def test_get_public_builder_by_domain_name(api_client, data_fixture):
 
     del response_json["theme"]  # We are not testing the theme response here.
 
-    assert builder_to.page_set.filter(shared=True).count() == 1
+    assert (
+        builder_to.page_set(manager="objects_with_shared").filter(shared=True).count()
+        == 1
+    )
 
-    shared_page = builder_to.page_set.get(shared=True)
+    shared_page = builder_to.shared_page
 
     assert response_json == {
         "favicon_file": UserFileSerializer(builder_to.favicon_file).data,
@@ -255,9 +258,12 @@ def test_get_public_builder_by_id(api_client, data_fixture):
 
     del response_json["theme"]  # We are not testing the theme response here.
 
-    assert page.builder.page_set.filter(shared=True).count() == 1
+    assert (
+        page.builder.page_set(manager="objects_with_shared").filter(shared=True).count()
+        == 1
+    )
 
-    shared_page = page.builder.page_set.get(shared=True)
+    shared_page = page.builder.shared_page
 
     assert response_json == {
         "favicon_file": UserFileSerializer(page.builder.favicon_file).data,
diff --git a/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py b/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py
index cecab34eb..e6360b7bb 100644
--- a/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py
+++ b/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py
@@ -359,7 +359,7 @@ def test_update_page_page_does_not_exist(api_client, data_fixture):
 def test_update_shared_page(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
 
     url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
     response = api_client.patch(
@@ -610,7 +610,7 @@ def test_order_pages_page_not_in_builder(api_client, data_fixture):
 def test_order_pages_shared_page(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
     page_one = data_fixture.create_builder_page(builder=builder, order=1)
 
     url = reverse(
@@ -698,7 +698,7 @@ def test_delete_page_page_not_exist(api_client, data_fixture):
 def test_delete_shared_page(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
 
     url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
     response = api_client.delete(
diff --git a/backend/tests/baserow/contrib/builder/api/test_builder_application_views.py b/backend/tests/baserow/contrib/builder/api/test_builder_application_views.py
index 1f921a2bf..339ca8e46 100644
--- a/backend/tests/baserow/contrib/builder/api/test_builder_application_views.py
+++ b/backend/tests/baserow/contrib/builder/api/test_builder_application_views.py
@@ -174,7 +174,7 @@ def test_get_builder_application(api_client, data_fixture):
         "login_page_id": None,
         "pages": [
             {
-                "id": application.page_set.get(shared=True).id,
+                "id": application.shared_page.id,
                 "builder_id": application.id,
                 "order": 1,
                 "name": "__shared__",
@@ -233,7 +233,7 @@ def test_list_builder_applications(api_client, data_fixture):
             "login_page_id": None,
             "pages": [
                 {
-                    "id": application.page_set.get(shared=True).id,
+                    "id": application.shared_page.id,
                     "builder_id": application.id,
                     "order": 1,
                     "name": "__shared__",
diff --git a/backend/tests/baserow/contrib/builder/api/test_builder_serializer.py b/backend/tests/baserow/contrib/builder/api/test_builder_serializer.py
index 3230400a8..4e34d4888 100644
--- a/backend/tests/baserow/contrib/builder/api/test_builder_serializer.py
+++ b/backend/tests/baserow/contrib/builder/api/test_builder_serializer.py
@@ -48,7 +48,7 @@ def test_validate_login_page_id_raises_error_if_shared_page(
     builder = builder_fixture["builder"]
 
     # Set the builder's page to be the shared page
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.page_set(manager="objects_with_shared").get(shared=True)
     response = api_client.patch(
         reverse("api:applications:item", kwargs={"application_id": builder.id}),
         {"login_page_id": shared_page.id},
diff --git a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
index 50e785235..4741570f4 100644
--- a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
+++ b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
@@ -717,7 +717,7 @@ def test_dispatch_local_baserow_update_row_workflow_action_using_formula_with_da
     integration = data_fixture.create_local_baserow_integration(
         user=user, application=builder
     )
-    shared_page = builder.page_set.first()
+    shared_page = builder.shared_page
 
     shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
         user=user,
diff --git a/backend/tests/baserow/contrib/builder/data_sources/test_data_source_handler.py b/backend/tests/baserow/contrib/builder/data_sources/test_data_source_handler.py
index 2f0b6df7a..fc941962f 100644
--- a/backend/tests/baserow/contrib/builder/data_sources/test_data_source_handler.py
+++ b/backend/tests/baserow/contrib/builder/data_sources/test_data_source_handler.py
@@ -94,7 +94,7 @@ def test_get_data_sources(data_fixture, specific):
 @pytest.mark.django_db
 def test_get_data_sources_with_shared(data_fixture):
     page = data_fixture.create_builder_page()
-    shared_page = page.builder.page_set.get(shared=True)
+    shared_page = page.builder.shared_page
     data_source1 = data_fixture.create_builder_local_baserow_get_row_data_source(
         page=page
     )
diff --git a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
index 69d83f095..347135208 100644
--- a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
+++ b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
@@ -186,10 +186,7 @@ def test_domain_publishing(data_fixture):
     assert domain1.published_to is not None
     assert domain1.published_to.workspace is None
     assert domain1.published_to.page_set.count() == builder.page_set.count()
-    assert (
-        domain1.published_to.page_set.exclude(shared=True).first().element_set.count()
-        == 2
-    )
+    assert domain1.published_to.page_set.first().element_set.count() == 2
 
     assert Builder.objects.count() == 2
 
diff --git a/backend/tests/baserow/contrib/builder/elements/test_element_handler.py b/backend/tests/baserow/contrib/builder/elements/test_element_handler.py
index a9e1f026c..dc959f5c6 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_element_handler.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_element_handler.py
@@ -1,6 +1,7 @@
 from decimal import Decimal
 
 import pytest
+from rest_framework.exceptions import ValidationError as DRFValidationError
 
 from baserow.contrib.builder.elements.element_types import (
     ColumnElementType,
@@ -27,9 +28,13 @@ def pytest_generate_tests(metafunc):
 @pytest.mark.django_db
 def test_create_element(data_fixture, element_type):
     page = data_fixture.create_builder_page()
+    shared_page = page.builder.shared_page
 
     pytest_params = element_type.get_pytest_params(data_fixture)
 
+    if element_type.is_multi_page_element:
+        page = shared_page
+
     element = ElementHandler().create_element(element_type, page=page, **pytest_params)
 
     assert element.page.id == page.id
@@ -41,6 +46,34 @@ def test_create_element(data_fixture, element_type):
     assert Element.objects.count() == 1
 
 
+@pytest.mark.django_db
+def test_create_element_and_shared_page(data_fixture):
+    page = data_fixture.create_builder_page()
+    shared_page = page.builder.shared_page
+
+    regular_element_type = next(
+        filter(lambda t: not t.is_multi_page_element, element_type_registry.get_all())
+    )
+
+    with pytest.raises(DRFValidationError):
+        ElementHandler().create_element(
+            regular_element_type,
+            page=shared_page,
+            **regular_element_type.get_pytest_params(data_fixture),
+        )
+
+    shared_element_type = next(
+        filter(lambda t: t.is_multi_page_element, element_type_registry.get_all())
+    )
+
+    with pytest.raises(DRFValidationError):
+        ElementHandler().create_element(
+            shared_element_type,
+            page=page,
+            **regular_element_type.get_pytest_params(data_fixture),
+        )
+
+
 @pytest.mark.django_db
 def test_get_element(data_fixture):
     element = data_fixture.create_builder_heading_element()
diff --git a/backend/tests/baserow/contrib/builder/elements/test_element_service.py b/backend/tests/baserow/contrib/builder/elements/test_element_service.py
index 2475c48cb..a4f8fd207 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_element_service.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_element_service.py
@@ -26,6 +26,11 @@ def pytest_generate_tests(metafunc):
 def test_create_element(element_created_mock, data_fixture, element_type):
     user = data_fixture.create_user()
     page = data_fixture.create_builder_page(user=user)
+    shared_page = page.builder.shared_page
+
+    if element_type.is_multi_page_element:
+        page = shared_page
+
     element1 = data_fixture.create_builder_heading_element(page=page, order="1.0000")
     element3 = data_fixture.create_builder_heading_element(page=page, order="2.0000")
 
diff --git a/backend/tests/baserow/contrib/builder/elements/test_element_types.py b/backend/tests/baserow/contrib/builder/elements/test_element_types.py
index d44e642f4..ed5303982 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_element_types.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_element_types.py
@@ -290,10 +290,13 @@ def test_form_container_element_import_export_formula(data_fixture):
         element_type.type
         for element_type in element_type_registry.get_all()
         if element_type.type != FormContainerElementType.type
+        and not element_type.is_multi_page_element
     ],
 )
 def test_form_container_child_types_allowed(allowed_element_type):
-    assert allowed_element_type in FormContainerElementType().child_types_allowed
+    assert allowed_element_type in [
+        e.type for e in FormContainerElementType().child_types_allowed
+    ]
 
 
 @pytest.mark.django_db
@@ -1347,10 +1350,13 @@ def test_choice_element_integer_option_values(data_fixture):
         element_type.type
         for element_type in element_type_registry.get_all()
         if element_type.type != ColumnElementType.type
+        and not element_type.is_multi_page_element
     ],
 )
 def test_column_container_child_types_allowed(allowed_element_type):
-    assert allowed_element_type in ColumnElementType().child_types_allowed
+    assert allowed_element_type in [
+        e.type for e in ColumnElementType().child_types_allowed
+    ]
 
 
 @pytest.mark.django_db
@@ -1513,7 +1519,7 @@ def test_repeat_element_import_export(data_fixture):
     imported_field = imported_table.field_set.get()
 
     # Pluck out the imported builder records.
-    imported_page = imported_builder.page_set.filter(shared=False).all()[0]
+    imported_page = imported_builder.page_set.all()[0]
     imported_data_source = imported_page.datasource_set.get()
     imported_root_repeat = imported_page.element_set.get(
         parent_element_id=None
diff --git a/backend/tests/baserow/contrib/builder/elements/test_header_footer_element_type.py b/backend/tests/baserow/contrib/builder/elements/test_header_footer_element_type.py
new file mode 100644
index 000000000..8893fb114
--- /dev/null
+++ b/backend/tests/baserow/contrib/builder/elements/test_header_footer_element_type.py
@@ -0,0 +1,93 @@
+import pytest
+
+from baserow.contrib.builder.elements.element_types import (
+    FooterElementType,
+    HeaderElementType,
+)
+from baserow.contrib.builder.elements.handler import ElementHandler
+from baserow.contrib.builder.elements.registries import element_type_registry
+
+
+def test_header_footer_child_types_allowed():
+    assert sorted([e.type for e in HeaderElementType().child_types_allowed]) == sorted(
+        [
+            element_type.type
+            for element_type in element_type_registry.get_all()
+            if not element_type.is_multi_page_element
+        ]
+    )
+
+    assert sorted([e.type for e in FooterElementType().child_types_allowed]) == sorted(
+        [
+            element_type.type
+            for element_type in element_type_registry.get_all()
+            if not element_type.is_multi_page_element
+        ]
+    )
+
+
+# Test prepare value
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+    "element_type", [HeaderElementType.type, FooterElementType.type]
+)
+def test_header_footer_prepare_value_for_db(data_fixture, element_type):
+    page = data_fixture.create_builder_page()
+    page1 = data_fixture.create_builder_page(builder=page.builder)
+    page2 = data_fixture.create_builder_page(builder=page.builder)
+    page3 = data_fixture.create_builder_page(builder=page.builder)
+    page4 = data_fixture.create_builder_page()
+    shared_page = page.builder.shared_page
+
+    element_type = element_type_registry.get(element_type)
+
+    created_element = ElementHandler().create_element(
+        element_type,
+        page=shared_page,
+        share_type="only",
+        pages=[page1, page2, page4, shared_page],
+    )
+
+    assert sorted([p.id for p in created_element.pages.all()]) == sorted(
+        [page1.id, page2.id]
+    )
+
+    updated_element = ElementHandler().update_element(
+        created_element,
+        pages=[page1, page4, shared_page],
+    )
+
+    assert sorted([p.id for p in updated_element.pages.all()]) == sorted([page1.id])
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+    "element_type", [HeaderElementType.type, FooterElementType.type]
+)
+def test_header_footer_import_with_id_mapping(data_fixture, element_type):
+    page = data_fixture.create_builder_page()
+    page42 = data_fixture.create_builder_page()
+    page43 = data_fixture.create_builder_page()
+
+    SERIALIZED_HEADER = {
+        "id": 42,
+        "type": element_type,
+        "share_type": "only",
+        "parent_element_id": None,
+        "pages": [42, 43],
+    }
+
+    cache = {}
+    id_mapping = {"builder_pages": {42: page42, 43: page43}}
+
+    created_element = ElementHandler().import_element(
+        page,
+        SERIALIZED_HEADER,
+        id_mapping,
+        cache=cache,
+    )
+
+    # We keep only the pages that are in the same builder
+    assert sorted([p.id for p in created_element.pages.all()]) == sorted(
+        [page42.id, page43.id]
+    )
diff --git a/backend/tests/baserow/contrib/builder/elements/test_record_selector_element_type.py b/backend/tests/baserow/contrib/builder/elements/test_record_selector_element_type.py
index 04e556828..27e4cbb31 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_record_selector_element_type.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_record_selector_element_type.py
@@ -93,12 +93,7 @@ def test_export_import_record_selector_element(data_fixture):
         import_export_config=config,
     )
     imported_builder = imported_apps[-1]
-    imported_element = (
-        imported_builder.page_set.filter(shared=False)
-        .first()
-        .element_set.first()
-        .specific
-    )
+    imported_element = imported_builder.page_set.first().element_set.first().specific
 
     # Check that the formula for option name suffix was updated with the new mapping
     import_option_name_suffix = imported_element.option_name_suffix
diff --git a/backend/tests/baserow/contrib/builder/pages/test_page_handler.py b/backend/tests/baserow/contrib/builder/pages/test_page_handler.py
index 39929d49e..e9affb3b0 100644
--- a/backend/tests/baserow/contrib/builder/pages/test_page_handler.py
+++ b/backend/tests/baserow/contrib/builder/pages/test_page_handler.py
@@ -94,7 +94,7 @@ def test_delete_page(data_fixture):
 @pytest.mark.django_db
 def test_delete_shared_page(data_fixture):
     page = data_fixture.create_builder_page()
-    shared_page = page.builder.page_set.get(shared=True)
+    shared_page = page.builder.shared_page
 
     with pytest.raises(SharedPageIsReadOnly):
         PageHandler().delete_page(shared_page)
@@ -114,7 +114,7 @@ def test_update_page(data_fixture):
 @pytest.mark.django_db
 def test_update_shared_page(data_fixture):
     page = data_fixture.create_builder_page(name="test")
-    shared_page = page.builder.page_set.get(shared=True)
+    shared_page = page.builder.shared_page
 
     with pytest.raises(SharedPageIsReadOnly):
         PageHandler().update_page(shared_page, name="new")
@@ -158,7 +158,7 @@ def test_order_pages(data_fixture):
 @pytest.mark.django_db
 def test_order_pages_page_not_in_builder(data_fixture):
     builder = data_fixture.create_builder_application()
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
     page_one = data_fixture.create_builder_page(builder=builder, order=1)
     page_two = data_fixture.create_builder_page(builder=builder, order=2)
 
@@ -189,7 +189,7 @@ def test_duplicate_page(data_fixture):
 @pytest.mark.django_db
 def test_duplicate_shared_page(data_fixture):
     page = data_fixture.create_builder_page()
-    shared_page = page.builder.page_set.get(shared=True)
+    shared_page = page.builder.shared_page
 
     with pytest.raises(SharedPageIsReadOnly):
         PageHandler().duplicate_page(shared_page)
diff --git a/backend/tests/baserow/contrib/builder/test_builder_application_type.py b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
index eb13e20cc..bef68741e 100644
--- a/backend/tests/baserow/contrib/builder/test_builder_application_type.py
+++ b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
@@ -60,11 +60,11 @@ def test_builder_application_type_init_application(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    assert Page.objects.count() == 1  # The shared page must exists
+    assert Page.objects.count() == 0
 
     BuilderApplicationType().init_application(user, builder)
 
-    assert Page.objects.count() == 3  # With demo data
+    assert Page.objects.count() == 2  # With demo data
 
 
 @pytest.mark.django_db
@@ -119,7 +119,7 @@ def test_builder_application_export(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
     page1 = data_fixture.create_builder_page(builder=builder)
     page2 = data_fixture.create_builder_page(builder=builder)
 
@@ -1002,8 +1002,11 @@ def test_builder_application_import(data_fixture):
     )
 
     assert builder.id != serialized_values["id"]
-    assert builder.page_set.exclude(shared=True).count() == 2
-    assert builder.page_set.filter(shared=True).count() == 1
+    assert builder.page_set.count() == 2
+    # ensure we have the shared page even if it's not in the reference
+    assert (
+        builder.page_set(manager="objects_with_shared").filter(shared=True).count() == 1
+    )
 
     assert builder.integrations.count() == 1
     first_integration = builder.integrations.first().specific
@@ -1011,7 +1014,7 @@ def test_builder_application_import(data_fixture):
 
     assert builder.user_sources.count() == 1
 
-    [page1, page2] = builder.page_set.exclude(shared=True)
+    [page1, page2] = builder.page_set.all()
 
     assert page1.element_set.count() == 6
     assert page2.element_set.count() == 1
@@ -1371,7 +1374,7 @@ def test_builder_application_imports_correct_default_roles(data_fixture):
         workspace, serialized_values, config, {}
     )
 
-    new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
+    new_element = builder.page_set.first().element_set.all()[0]
     new_user_source = builder.user_sources.all()[0]
 
     # Ensure the "old" Default User Role doesn't exist
@@ -1455,7 +1458,7 @@ def test_ensure_new_element_roles_are_sanitized_during_import_for_default_roles(
     expected_roles = _expected_roles
 
     # Ensure new element has roles updated
-    new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
+    new_element = builder.page_set.all()[0].element_set.all()[0]
     for index, role in enumerate(new_element.roles):
         # Default Role's User Source should have changed for new elements
         if role.startswith(prefix):
@@ -1532,5 +1535,5 @@ def test_ensure_new_element_roles_are_sanitized_during_import_for_roles(
             workspace, serialized, config, {}
         )
 
-    new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
+    new_element = builder.page_set.all()[0].element_set.all()[0]
     assert new_element.roles == expected_roles
diff --git a/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py b/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
index f5254016f..68249ab43 100644
--- a/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
+++ b/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
@@ -901,7 +901,7 @@ def test_get_builder_used_property_names_returns_merged_property_names_integrati
     integration = data_fixture.create_local_baserow_integration(
         user=user, application=builder
     )
-    shared_page = builder.page_set.get(shared=True)
+    shared_page = builder.shared_page
     page = data_fixture.create_builder_page(builder=builder)
     page2 = data_fixture.create_builder_page(builder=builder)
 
diff --git a/backend/tests/baserow/contrib/builder/test_permissions_manager.py b/backend/tests/baserow/contrib/builder/test_permissions_manager.py
index 7f6979471..344ee0af1 100755
--- a/backend/tests/baserow/contrib/builder/test_permissions_manager.py
+++ b/backend/tests/baserow/contrib/builder/test_permissions_manager.py
@@ -182,7 +182,7 @@ def test_allow_if_template_permission_manager_filter_queryset(data_fixture):
     workspace_2 = data_fixture.create_workspace()
     data_fixture.create_template(workspace=workspace_2)
     application_2 = data_fixture.create_builder_application(workspace=workspace_2)
-    shared_page_2 = application_2.page_set.get(shared=True)
+    shared_page_2 = application_2.shared_page
     page_2 = data_fixture.create_builder_page(builder=application_2)
     element_2 = data_fixture.create_builder_text_element(page=page_2)
     workflow_action_2 = data_fixture.create_local_baserow_update_row_workflow_action(
@@ -230,7 +230,7 @@ def test_allow_if_template_permission_manager_filter_queryset(data_fixture):
     tests_w1 = [
         (
             ListPagesBuilderOperationType.type,
-            Page.objects.filter(builder__workspace=workspace_2),
+            Page.objects_with_shared.filter(builder__workspace=workspace_2),
             [shared_page_2.id, page_2.id],
         ),
         (
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
index 8343ddc1a..13b5b89c7 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
@@ -603,7 +603,7 @@ def test_export_import_local_baserow_upsert_row_service(
     imported_table = imported_database.table_set.get()
     imported_field = imported_table.field_set.get()
 
-    imported_page = imported_builder.page_set.exclude(shared=True).get()
+    imported_page = imported_builder.page_set.get()
     imported_data_source = imported_page.datasource_set.get()
     imported_integration = imported_builder.integrations.get()
     imported_upsert_row_service = LocalBaserowUpsertRow.objects.get(
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py b/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py
index 1fedc71d8..79a679004 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py
@@ -169,7 +169,7 @@ def test_local_baserow_table_service_filterable_mixin_import_export(data_fixture
     imported_select_option = imported_single_select_field.select_options.get()
 
     # Pluck out the imported builder records.
-    imported_page = imported_builder.page_set.filter(shared=False).get()
+    imported_page = imported_builder.page_set.get()
     imported_datasource = imported_page.datasource_set.get()
     imported_filters = [
         {"field_id": sf.field_id, "value": sf.value}
diff --git a/changelog/entries/unreleased/feature/2486_builder_add_the_multipage_header_and_footer_containers.json b/changelog/entries/unreleased/feature/2486_builder_add_the_multipage_header_and_footer_containers.json
new file mode 100644
index 000000000..aaae36dd3
--- /dev/null
+++ b/changelog/entries/unreleased/feature/2486_builder_add_the_multipage_header_and_footer_containers.json
@@ -0,0 +1,7 @@
+{
+    "type": "feature",
+    "message": "[Builder] Add the multi-page header and footer containers",
+    "issue_number": 2486,
+    "bullet_points": [],
+    "created_at": "2024-10-29"
+}
\ No newline at end of file
diff --git a/e2e-tests/tests/builder/builderPage.spec.ts b/e2e-tests/tests/builder/builderPage.spec.ts
index 05b50a350..ce85491aa 100644
--- a/e2e-tests/tests/builder/builderPage.spec.ts
+++ b/e2e-tests/tests/builder/builderPage.spec.ts
@@ -58,7 +58,7 @@ test.describe("Builder page test suite", () => {
   });
 
   test("Can create an element from empty page", async ({ page }) => {
-    await page.getByText("Click to create first element").click();
+    await page.getByText("Click to create an element").click();
     await page.getByText("Heading", { exact: true }).click();
 
     await expect(
diff --git a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py
index fe4a2220d..e87b70786 100644
--- a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py
+++ b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py
@@ -957,7 +957,7 @@ def test_public_dispatch_data_source_with_ab_user_using_user_source(
     refresh_token = user_source_user.get_refresh_token()
     access_token = refresh_token.access_token
 
-    published_page = domain1.published_to.page_set.exclude(shared=True).first()
+    published_page = domain1.published_to.page_set.first()
     published_data_source = published_page.datasource_set.first()
 
     url = reverse(
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/builder/components/elements/AuthFormElement.vue b/enterprise/web-frontend/modules/baserow_enterprise/builder/components/elements/AuthFormElement.vue
index 664b8f650..54c6a1ec1 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/builder/components/elements/AuthFormElement.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/builder/components/elements/AuthFormElement.vue
@@ -65,7 +65,7 @@ import { mapActions } from 'vuex'
 export default {
   name: 'AuthFormElement',
   mixins: [element, form, error],
-  inject: ['page', 'builder'],
+  inject: ['elementPage', 'builder'],
   props: {
     /**
      * @type {Object}
@@ -128,7 +128,7 @@ export default {
           if (!found) {
             // If the user_source has been removed we need to update the element
             this.actionForceUpdateElement({
-              page: this.page,
+              page: this.elementPage,
               element: this.element,
               values: { user_source_id: null },
             })
diff --git a/web-frontend/modules/builder/applicationTypes.js b/web-frontend/modules/builder/applicationTypes.js
index 1ea0e8172..6018ed554 100644
--- a/web-frontend/modules/builder/applicationTypes.js
+++ b/web-frontend/modules/builder/applicationTypes.js
@@ -86,9 +86,10 @@ export class BuilderApplicationType extends ApplicationType {
     }
   }
 
-  async loadExtraData(builder, page, mode) {
+  async loadExtraData(builder, mode) {
     const { store, $registry } = this.app
     if (!builder._loadedOnce) {
+      const sharedPage = store.getters['page/getSharedPage'](builder)
       await Promise.all([
         store.dispatch('userSource/fetch', {
           application: builder,
@@ -98,8 +99,12 @@ export class BuilderApplicationType extends ApplicationType {
         }),
         // Fetch shared data sources
         store.dispatch('dataSource/fetch', {
-          page: store.getters['page/getSharedPage'](builder),
+          page: sharedPage,
         }),
+        store.dispatch('element/fetch', {
+          page: sharedPage,
+        }),
+        store.dispatch('workflowAction/fetch', { page: sharedPage }),
       ])
 
       // Initialize application shared stuff like data sources
diff --git a/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue b/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue
index aaf8b46ae..ed990aece 100644
--- a/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue
+++ b/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue
@@ -19,8 +19,8 @@ export default {
   components: { FormulaInputField },
   mixins: [applicationContext],
   inject: {
-    page: {
-      from: 'page',
+    elementPage: {
+      from: 'elementPage',
     },
     builder: {
       from: 'builder',
@@ -38,10 +38,12 @@ export default {
   },
   computed: {
     dataSourceLoading() {
-      return this.$store.getters['dataSource/getLoading'](this.page)
+      return this.$store.getters['dataSource/getLoading'](this.elementPage)
     },
     dataSourceContentLoading() {
-      return this.$store.getters['dataSourceContent/getLoading'](this.page)
+      return this.$store.getters['dataSourceContent/getLoading'](
+        this.elementPage
+      )
     },
     dataProviders() {
       return this.dataProvidersAllowed.map((dataProviderName) =>
diff --git a/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue b/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue
index 19a5bb106..b7be94dc5 100644
--- a/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue
+++ b/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue
@@ -61,7 +61,16 @@ export default {
   components: { DataSourceForm },
 
   mixins: [modal, error],
-  inject: ['builder', 'page'],
+  provide() {
+    // I know, it's not the page of the element but it's injected into the
+    // ApplicationBuilderFormulaInput for data source loading states,
+    // and we need the right page which can be in fact the data source page in this
+    // case, so it works.
+    // May be we could change the name of the elementPage but it would be only for
+    // this exception.
+    return { elementPage: this.dataSourcePage }
+  },
+  inject: ['builder', 'currentPage'],
   props: {
     dataSourceId: { type: Number, required: false, default: null },
   },
@@ -74,7 +83,9 @@ export default {
   },
   computed: {
     dataSources() {
-      return this.$store.getters['dataSource/getPageDataSources'](this.page)
+      return this.$store.getters['dataSource/getPageDataSources'](
+        this.currentPage
+      )
     },
     sharedPage() {
       return this.$store.getters['page/getSharedPage'](this.builder)
@@ -108,7 +119,7 @@ export default {
     // edited. Sometimes it's the shared page.
     dataSourcePage() {
       if (!this.dataSource) {
-        return this.page
+        return this.currentPage
       }
       return this.$store.getters['page/getById'](
         this.builder,
@@ -116,7 +127,11 @@ export default {
       )
     },
     elements() {
-      return this.$store.getters['element/getElementsOrdered'](this.page)
+      // This is used when we want to dispatch the data source update
+      return [
+        ...this.$store.getters['element/getElementsOrdered'](this.currentPage),
+        ...this.$store.getters['element/getElementsOrdered'](this.sharedPage),
+      ]
     },
   },
   methods: {
@@ -145,7 +160,7 @@ export default {
       try {
         if (this.create) {
           const createdDataSource = await this.actionCreateDataSource({
-            page: this.page,
+            page: this.currentPage,
             values,
           })
           this.actualDataSourceId = createdDataSource.id
diff --git a/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue b/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
index eb0e3125b..05fd06423 100644
--- a/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
+++ b/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
@@ -17,18 +17,24 @@
         :icon-tooltip="$t('dataSourceDropdown.shared')"
       >
       </DropdownItem>
-      <DropdownItem
-        v-for="dataSource in pageDataSources"
-        :key="dataSource.id"
-        :name="getDataSourceLabel(dataSource)"
-        :value="dataSource.id"
-        icon="iconoir-empty-page"
-        :icon-tooltip="$t('dataSourceDropdown.pageOnly')"
-      >
-      </DropdownItem>
+      <template v-if="localDataSources">
+        <DropdownItem
+          v-for="dataSource in localDataSources"
+          :key="dataSource.id"
+          :name="getDataSourceLabel(dataSource)"
+          :value="dataSource.id"
+          icon="iconoir-empty-page"
+          :icon-tooltip="$t('dataSourceDropdown.pageOnly')"
+        >
+        </DropdownItem
+      ></template>
       <template #emptyState>
-        <slot name="emptyState"
-          >{{ $t('dataSourceDropdown.noDataSources') }}
+        <slot name="emptyState">
+          {{
+            isOnSharedPage
+              ? $t('dataSourceDropdown.noSharedDataSources')
+              : $t('dataSourceDropdown.noDataSources')
+          }}
         </slot>
       </template>
     </Dropdown>
@@ -44,30 +50,24 @@ export default {
       required: false,
       default: null,
     },
-    dataSources: {
+    sharedDataSources: {
       type: Array,
       required: true,
     },
+    localDataSources: {
+      type: Array,
+      required: false,
+      default: null,
+    },
     small: {
       type: Boolean,
       required: false,
       default: false,
     },
-    page: {
-      type: Object,
-      required: true,
-    },
   },
   computed: {
-    pageDataSources() {
-      return this.dataSources.filter(
-        ({ page_id: pageId }) => pageId === this.page.id
-      )
-    },
-    sharedDataSources() {
-      return this.dataSources.filter(
-        ({ page_id: pageId }) => pageId !== this.page.id
-      )
+    isOnSharedPage() {
+      return this.localDataSources === null
     },
   },
   methods: {
diff --git a/web-frontend/modules/builder/components/elements/AddElementCard.vue b/web-frontend/modules/builder/components/elements/AddElementCard.vue
index 5cf6b5d12..5d93fcdcc 100644
--- a/web-frontend/modules/builder/components/elements/AddElementCard.vue
+++ b/web-frontend/modules/builder/components/elements/AddElementCard.vue
@@ -1,7 +1,7 @@
 <template>
   <div
     :key="elementType.name"
-    v-tooltip="disallowedTypeForAncestry ? disabledElementMessage : null"
+    v-tooltip="disabled ? disabledMessage : null"
     class="add-element-card"
     :class="{ 'add-element-card--disabled': disabled }"
     v-on="$listeners"
@@ -30,20 +30,15 @@ export default {
       type: Object,
       required: true,
     },
-    parentType: {
-      type: Object,
-      required: false,
-      default: null,
-    },
     loading: {
       type: Boolean,
       required: false,
       default: false,
     },
-    disallowedTypeForAncestry: {
-      type: Boolean,
+    disabledMessage: {
+      type: String,
       required: false,
-      default: false,
+      default: '',
     },
     disabled: {
       type: Boolean,
@@ -51,10 +46,5 @@ export default {
       default: false,
     },
   },
-  computed: {
-    disabledElementMessage() {
-      return this.$t('addElementModal.disabledElementTooltip')
-    },
-  },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/elements/AddElementModal.vue b/web-frontend/modules/builder/components/elements/AddElementModal.vue
index 4c4cf6761..591a25cc3 100644
--- a/web-frontend/modules/builder/components/elements/AddElementModal.vue
+++ b/web-frontend/modules/builder/components/elements/AddElementModal.vue
@@ -13,10 +13,9 @@
         v-for="elementType in elementTypes"
         :key="elementType.getType()"
         :element-type="elementType"
-        :parent-type="parentElementType"
-        :disallowed-type-for-ancestry="isDisallowedByParent(elementType)"
         :loading="addingElementType === elementType.getType()"
-        :disabled="isCardDisabled(elementType)"
+        :disabled="isElementTypeDisabled(elementType)"
+        :disabled-message="getElementTypeDisabledMessage(elementType)"
         @click="addElement(elementType)"
       />
     </div>
@@ -29,21 +28,18 @@ import AddElementCard from '@baserow/modules/builder/components/elements/AddElem
 import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
 import { notifyIf } from '@baserow/modules/core/utils/error'
 import { mapActions } from 'vuex'
+import { PAGE_PLACES } from '../../enums'
 
 export default {
   name: 'AddElementModal',
   components: { AddElementCard },
   mixins: [modal],
+  inject: ['builder', 'currentPage'],
   props: {
     page: {
       type: Object,
       required: true,
     },
-    elementTypesAllowed: {
-      type: Array,
-      required: false,
-      default: null,
-    },
   },
   data() {
     return {
@@ -51,6 +47,7 @@ export default {
       placeInContainer: null,
       beforeId: null,
       parentElementId: null,
+      pagePlace: null,
       addingElementType: null,
     }
   },
@@ -64,57 +61,101 @@ export default {
         )
       )
     },
-    parentElementType() {
-      const parentElement = this.$store.getters['element/getElementById'](
-        this.page,
-        this.parentElementId
-      )
-      return parentElement
-        ? this.$registry.get('element', parentElement.type)
-        : null
+    sharedPage() {
+      return this.$store.getters['page/getSharedPage'](this.builder)
+    },
+    parentElement() {
+      if (this.parentElementId) {
+        return this.$store.getters['element/getElementByIdInPages'](
+          [this.currentPage, this.sharedPage],
+          this.parentElementId
+        )
+      }
+      return null
+    },
+    beforeElement() {
+      if (this.beforeId) {
+        return this.$store.getters['element/getElementByIdInPages'](
+          [this.currentPage, this.sharedPage],
+          this.beforeId
+        )
+      }
+      return null
     },
   },
   methods: {
-    isDisallowedByParent(elementType) {
-      return (
-        this.elementTypesAllowed !== null &&
-        !this.elementTypesAllowed.includes(elementType)
-      )
+    getElementTypeDisabledMessage(elementType) {
+      if (elementType.getType() === this.addingElementType) {
+        // This type is disabled while we add it.
+        return this.$t('addElementModal.elementInProgress')
+      }
+
+      return elementType.isDisallowedReason({
+        builder: this.builder,
+        page: this.page,
+        placeInContainer: this.placeInContainer,
+        parentElement: this.parentElement,
+        beforeElement: this.beforeElement,
+        pagePlace: this.pagePlace,
+      })
     },
-    isCardDisabled(elementType) {
-      const isAddingElementType =
-        this.addingElementType !== null &&
-        elementType.getType() === this.addingElementType
-      return isAddingElementType || this.isDisallowedByParent(elementType)
+    isElementTypeDisabled(elementType) {
+      return !!this.getElementTypeDisabledMessage(elementType)
     },
     ...mapActions({
       actionCreateElement: 'element/create',
     }),
 
-    show({ placeInContainer, beforeId, parentElementId } = {}, ...args) {
+    show(
+      { placeInContainer, beforeId, parentElementId, pagePlace } = {},
+      ...args
+    ) {
       this.placeInContainer = placeInContainer
       this.beforeId = beforeId
       this.parentElementId = parentElementId
+      this.pagePlace = pagePlace
       modal.methods.show.bind(this)(...args)
     },
+
     async addElement(elementType) {
-      if (this.isCardDisabled(elementType)) {
+      if (this.isElementTypeDisabled(elementType)) {
         return false
       }
       this.addingElementType = elementType.getType()
-      const configuration = this.parentElementId
-        ? {
-            parent_element_id: this.parentElementId,
-            place_in_container: this.placeInContainer,
-          }
-        : null
+
+      let beforeId = this.beforeId
+      let destinationPage
+
+      if (this.parentElementId) {
+        // The page must be the same as the parent one
+        destinationPage =
+          this.parentElement.page_id === this.currentPage.id
+            ? this.currentPage
+            : this.sharedPage
+      } else {
+        // The page is forced by the element type page place
+        destinationPage =
+          elementType.getPagePlace() === PAGE_PLACES.CONTENT
+            ? this.currentPage
+            : this.sharedPage
+        // If the before element doesn't belong to the same page we must ignore it
+        if (
+          this.beforeElement &&
+          this.beforeElement.page_id !== destinationPage.id
+        ) {
+          beforeId = null
+        }
+      }
 
       try {
         await this.actionCreateElement({
-          page: this.page,
+          page: destinationPage,
           elementType: elementType.getType(),
-          beforeId: this.beforeId,
-          configuration,
+          beforeId,
+          values: {
+            parent_element_id: this.parentElementId,
+            place_in_container: this.placeInContainer,
+          },
         })
 
         this.$emit('element-added')
diff --git a/web-frontend/modules/builder/components/elements/AddElementZone.vue b/web-frontend/modules/builder/components/elements/AddElementZone.vue
index 7f29253a1..ab608b675 100644
--- a/web-frontend/modules/builder/components/elements/AddElementZone.vue
+++ b/web-frontend/modules/builder/components/elements/AddElementZone.vue
@@ -1,9 +1,12 @@
 <template>
-  <div class="add-element-zone" @click="!disabled && $emit('add-element')">
+  <div
+    class="add-element-zone"
+    :class="{ 'add-element-zone--disabled': disabled }"
+  >
     <div
       v-tooltip="disabled ? tooltip : null"
-      class="add-element-zone__content"
-      :class="{ 'add-element-zone__button--disabled': disabled }"
+      class="add-element-zone__button"
+      @click="!disabled && $emit('add-element')"
     >
       <i class="iconoir-plus add-element-zone__icon"></i>
     </div>
diff --git a/web-frontend/modules/builder/components/elements/ElementMenu.vue b/web-frontend/modules/builder/components/elements/ElementMenu.vue
index ab17280a4..1f92b9f1c 100644
--- a/web-frontend/modules/builder/components/elements/ElementMenu.vue
+++ b/web-frontend/modules/builder/components/elements/ElementMenu.vue
@@ -20,69 +20,62 @@
       </span>
     </a>
     <a
-      v-if="isPlacementVisible(PLACEMENTS.LEFT)"
+      v-if="isDirectionVisible(DIRECTIONS.LEFT)"
       class="element-preview__menu-item"
-      :class="{ disabled: isPlacementDisabled(PLACEMENTS.LEFT) }"
-      @click="
-        !isPlacementDisabled(PLACEMENTS.LEFT) && $emit('move', PLACEMENTS.LEFT)
-      "
+      :class="{
+        'element-preview__menu-item--disabled': !isAllowedDirection(
+          DIRECTIONS.LEFT
+        ),
+      }"
+      @click="$emit('move', DIRECTIONS.LEFT)"
     >
       <i class="iconoir-nav-arrow-left"></i>
-      <span
-        v-if="!isPlacementDisabled(PLACEMENTS.LEFT)"
-        class="element-preview__menu-item-description"
-      >
+      <span class="element-preview__menu-item-description">
         {{ $t('elementMenu.moveLeft') }}
       </span>
     </a>
     <a
-      v-if="isPlacementVisible(PLACEMENTS.RIGHT)"
+      v-if="isDirectionVisible(DIRECTIONS.RIGHT)"
       class="element-preview__menu-item"
-      :class="{ disabled: isPlacementDisabled(PLACEMENTS.RIGHT) }"
-      @click="
-        !isPlacementDisabled(PLACEMENTS.RIGHT) &&
-          $emit('move', PLACEMENTS.RIGHT)
-      "
+      :class="{
+        'element-preview__menu-item--disabled': !isAllowedDirection(
+          DIRECTIONS.RIGHT
+        ),
+      }"
+      @click="$emit('move', DIRECTIONS.RIGHT)"
     >
       <i class="iconoir-nav-arrow-right"></i>
-      <span
-        v-if="!isPlacementDisabled(PLACEMENTS.RIGHT)"
-        class="element-preview__menu-item-description"
-      >
+      <span class="element-preview__menu-item-description">
         {{ $t('elementMenu.moveRight') }}
       </span>
     </a>
     <a
-      v-if="isPlacementVisible(PLACEMENTS.BEFORE)"
+      v-if="isDirectionVisible(DIRECTIONS.BEFORE)"
       class="element-preview__menu-item"
-      :class="{ disabled: isPlacementDisabled(PLACEMENTS.BEFORE) }"
-      @click="
-        !isPlacementDisabled(PLACEMENTS.BEFORE) &&
-          $emit('move', PLACEMENTS.BEFORE)
-      "
+      :class="{
+        'element-preview__menu-item--disabled': !isAllowedDirection(
+          DIRECTIONS.BEFORE
+        ),
+      }"
+      @click="$emit('move', DIRECTIONS.BEFORE)"
     >
       <i class="iconoir-nav-arrow-up"></i>
-      <span
-        v-if="!isPlacementDisabled(PLACEMENTS.BEFORE)"
-        class="element-preview__menu-item-description"
-      >
+      <span class="element-preview__menu-item-description">
         {{ $t('elementMenu.moveUp') }}
       </span>
     </a>
     <a
-      v-if="isPlacementVisible(PLACEMENTS.AFTER)"
+      v-if="isDirectionVisible(DIRECTIONS.AFTER)"
       class="element-preview__menu-item"
-      :class="{ disabled: isPlacementDisabled(PLACEMENTS.AFTER) }"
-      @click="
-        !isPlacementDisabled(PLACEMENTS.AFTER) &&
-          $emit('move', PLACEMENTS.AFTER)
-      "
+      :class="{
+        'element-preview__menu-item--disabled': !isAllowedDirection(
+          DIRECTIONS.AFTER
+        ),
+      }"
+      @click="$emit('move', DIRECTIONS.AFTER)"
     >
       <i class="iconoir-nav-arrow-down"></i>
-      <span
-        v-if="!isPlacementDisabled(PLACEMENTS.AFTER)"
-        class="element-preview__menu-item-description"
-      >
+      <span class="element-preview__menu-item-description">
         {{ $t('elementMenu.moveDown') }}
       </span>
     </a>
@@ -96,7 +89,7 @@
 </template>
 
 <script>
-import { PLACEMENTS } from '@baserow/modules/builder/enums'
+import { DIRECTIONS } from '@baserow/modules/builder/enums'
 
 export default {
   name: 'ElementMenu',
@@ -111,26 +104,26 @@ export default {
       required: false,
       default: false,
     },
-    placements: {
+    directions: {
       type: Array,
       required: false,
-      default: () => [PLACEMENTS.BEFORE, PLACEMENTS.AFTER],
+      default: () => [DIRECTIONS.BEFORE, DIRECTIONS.AFTER],
     },
-    placementsDisabled: {
+    allowedDirections: {
       type: Array,
       required: false,
       default: () => [],
     },
   },
   computed: {
-    PLACEMENTS: () => PLACEMENTS,
+    DIRECTIONS: () => DIRECTIONS,
   },
   methods: {
-    isPlacementVisible(placement) {
-      return this.placements.includes(placement)
+    isDirectionVisible(direction) {
+      return this.directions.includes(direction)
     },
-    isPlacementDisabled(placement) {
-      return this.placementsDisabled.includes(placement)
+    isAllowedDirection(direction) {
+      return this.allowedDirections.includes(direction)
     },
   },
 }
diff --git a/web-frontend/modules/builder/components/elements/ElementPreview.vue b/web-frontend/modules/builder/components/elements/ElementPreview.vue
index 598f4d517..c1b658d6e 100644
--- a/web-frontend/modules/builder/components/elements/ElementPreview.vue
+++ b/web-frontend/modules/builder/components/elements/ElementPreview.vue
@@ -20,38 +20,37 @@
       v-show="isSelected"
       v-if="canCreate"
       class="element-preview__insert element-preview__insert--top"
-      @click="showAddElementModal(PLACEMENTS.BEFORE)"
+      @click="showAddElementModal(DIRECTIONS.BEFORE)"
     />
     <ElementMenu
       v-if="isSelected && canUpdate"
-      :placements="placements"
-      :placements-disabled="placementsDisabled"
+      :directions="directions"
+      :allowed-directions="allowedMoveDirections"
       :is-duplicating="isDuplicating"
       :has-parent="!!parentElement"
       @delete="deleteElement"
-      @move="$emit('move', $event)"
+      @move="onMove"
       @duplicate="duplicateElement"
       @select-parent="selectParentElement()"
     />
-
     <PageElement
       :element="element"
       :mode="mode"
       class="element--read-only"
       :application-context-additions="applicationContextAdditions"
+      v-on="$listeners"
     />
 
     <InsertElementButton
       v-show="isSelected"
       v-if="canCreate"
       class="element-preview__insert element-preview__insert--bottom"
-      @click="showAddElementModal(PLACEMENTS.AFTER)"
+      @click="showAddElementModal(DIRECTIONS.AFTER)"
     />
     <AddElementModal
       v-if="canCreate"
       ref="addElementModal"
-      :element-types-allowed="elementTypesAllowed"
-      :page="page"
+      :page="elementPage"
     />
 
     <i
@@ -65,7 +64,7 @@
 import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu'
 import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton'
 import PageElement from '@baserow/modules/builder/components/page/PageElement'
-import { PLACEMENTS } from '@baserow/modules/builder/enums'
+import { DIRECTIONS } from '@baserow/modules/builder/enums'
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
 import { notifyIf } from '@baserow/modules/core/utils/error'
 import { mapActions, mapGetters } from 'vuex'
@@ -85,27 +84,17 @@ export default {
     InsertElementButton,
     PageElement,
   },
-  inject: ['workspace', 'builder', 'page', 'mode'],
+  inject: ['workspace', 'builder', 'mode', 'currentPage'],
   props: {
     element: {
       type: Object,
       required: true,
     },
-    isLastElement: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
     isFirstElement: {
       type: Boolean,
       required: false,
       default: false,
     },
-    isRootElement: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
     applicationContextAdditions: {
       type: Object,
       required: false,
@@ -124,7 +113,23 @@ export default {
       getClosestSiblingElement: 'element/getClosestSiblingElement',
       loggedUser: 'userSourceUser/getUser',
     }),
+    elementPage() {
+      // We use the page from the element itself
+      return this.$store.getters['page/getById'](
+        this.builder,
+        this.element.page_id
+      )
+    },
     isVisible() {
+      if (
+        !this.elementType.isVisible({
+          element: this.element,
+          currentPage: this.currentPage,
+        })
+      ) {
+        return false
+      }
+
       const isAuthenticated = this.$store.getters[
         'userSourceUser/isAuthenticated'
       ](this.builder)
@@ -151,13 +156,13 @@ export default {
         return true
       }
     },
-    PLACEMENTS: () => PLACEMENTS,
-    placements() {
+    DIRECTIONS: () => DIRECTIONS,
+    directions() {
       return [
-        PLACEMENTS.BEFORE,
-        PLACEMENTS.AFTER,
-        PLACEMENTS.LEFT,
-        PLACEMENTS.RIGHT,
+        DIRECTIONS.BEFORE,
+        DIRECTIONS.AFTER,
+        DIRECTIONS.LEFT,
+        DIRECTIONS.RIGHT,
       ]
     },
     parentOfElementSelected() {
@@ -165,24 +170,34 @@ export default {
         return null
       }
       return this.$store.getters['element/getElementById'](
-        this.page,
+        this.elementPage,
         this.elementSelected.parent_element_id
       )
     },
-    placementsDisabled() {
-      const elementType = this.$registry.get('element', this.element.type)
-      return elementType.getPlacementsDisabled(this.page, this.element)
+    elementsAround() {
+      return this.elementType.getElementsAround({
+        builder: this.builder,
+        page: this.currentPage,
+        withSharedPage: true,
+        element: this.element,
+      })
     },
-    elementTypesAllowed() {
-      return (
-        this.parentElementType?.childElementTypes(this.page, this.element) ||
-        null
-      )
+    nextPlaces() {
+      return this.elementType.getNextPlaces({
+        builder: this.builder,
+        page: this.elementPage,
+        element: this.element,
+      })
+    },
+    allowedMoveDirections() {
+      return Object.entries(this.nextPlaces)
+        .filter(([, nextPlace]) => !!nextPlace)
+        .map(([direction]) => direction)
     },
     canCreate() {
       return this.$hasPermission(
         'builder.page.create_element',
-        this.page,
+        this.currentPage,
         this.workspace.id
       )
     },
@@ -200,7 +215,7 @@ export default {
       if (!this.elementSelected) {
         return []
       }
-      return this.elementAncestors(this.page, this.elementSelected).map(
+      return this.elementAncestors(this.elementPage, this.elementSelected).map(
         ({ id }) => id
       )
     },
@@ -215,7 +230,7 @@ export default {
         return null
       }
       return this.$store.getters['element/getElementById'](
-        this.page,
+        this.elementPage,
         this.element.parent_element_id
       )
     },
@@ -224,15 +239,9 @@ export default {
         ? this.$registry.get('element', this.parentElement?.type)
         : null
     },
-    nextElement() {
-      return this.$store.getters['element/getNextElement'](
-        this.page,
-        this.element
-      )
-    },
     inError() {
       return this.elementType.isInError({
-        page: this.page,
+        page: this.elementPage,
         element: this.element,
         builder: this.builder,
       })
@@ -284,6 +293,9 @@ export default {
       actionDeleteElement: 'element/delete',
       actionSelectElement: 'element/select',
     }),
+    onMove(direction) {
+      this.$emit('move', { element: this.element, direction })
+    },
     onSelect($event) {
       // Here we check that the event has been emitted for this particular element
       // If we found an intermediate DOM element with the class `element-preview`,
@@ -300,23 +312,32 @@ export default {
         this.actionSelectElement({ element: this.element })
       }
     },
-    showAddElementModal(placement) {
+    showAddElementModal(direction) {
+      const rootElement = this.$store.getters['element/getAncestors'](
+        this.elementPage,
+        this.element,
+        { includeSelf: true }
+      )[0]
+      const rootElementType = this.$registry.get('element', rootElement.type)
+      const pagePlace = rootElementType.getPagePlace()
+
       this.$refs.addElementModal.show({
         placeInContainer: this.element.place_in_container,
         parentElementId: this.element.parent_element_id,
-        beforeId: this.getBeforeId(placement),
+        beforeId: this.getBeforeId(direction),
+        pagePlace,
       })
     },
-    getBeforeId(placement) {
-      return placement === PLACEMENTS.BEFORE
+    getBeforeId(direction) {
+      return direction === DIRECTIONS.BEFORE
         ? this.element.id
-        : this.nextElement?.id || null
+        : this.elementsAround[DIRECTIONS.AFTER]?.id || null
     },
     async duplicateElement() {
       this.isDuplicating = true
       try {
         await this.actionDuplicateElement({
-          page: this.page,
+          page: this.elementPage,
           elementId: this.element.id,
         })
       } catch (error) {
@@ -326,12 +347,15 @@ export default {
     },
     async deleteElement() {
       try {
-        const siblingElementToSelect = this.getClosestSiblingElement(
-          this.page,
-          this.elementSelected
-        )
+        const siblingElementToSelect =
+          this.elementsAround[DIRECTIONS.AFTER] ||
+          this.elementsAround[DIRECTIONS.BEFORE] ||
+          this.elementsAround[DIRECTIONS.LEFT] ||
+          this.elementsAround[DIRECTIONS.RIGHT] ||
+          this.parentOfElementSelected
+
         await this.actionDeleteElement({
-          page: this.page,
+          page: this.elementPage,
           elementId: this.element.id,
         })
         if (siblingElementToSelect?.id) {
diff --git a/web-frontend/modules/builder/components/elements/ElementsList.vue b/web-frontend/modules/builder/components/elements/ElementsList.vue
index 7271614ff..c0af251b9 100644
--- a/web-frontend/modules/builder/components/elements/ElementsList.vue
+++ b/web-frontend/modules/builder/components/elements/ElementsList.vue
@@ -1,8 +1,5 @@
 <template>
-  <ul
-    v-auto-overflow-scroll
-    class="elements-list__items elements-list__items--no-max-height"
-  >
+  <ul class="elements-list">
     <ElementsListItem
       v-for="element in filteredElements"
       :key="element.id"
@@ -19,7 +16,6 @@ import ElementsListItem from '@baserow/modules/builder/components/elements/Eleme
 export default {
   name: 'ElementsList',
   components: { ElementsListItem },
-  inject: ['page'],
   props: {
     elements: {
       type: Array,
diff --git a/web-frontend/modules/builder/components/elements/ElementsListItem.vue b/web-frontend/modules/builder/components/elements/ElementsListItem.vue
index 9dc2f58c6..00941273e 100644
--- a/web-frontend/modules/builder/components/elements/ElementsListItem.vue
+++ b/web-frontend/modules/builder/components/elements/ElementsListItem.vue
@@ -1,15 +1,15 @@
 <template>
-  <li :key="element.id" class="elements-list__item">
-    <a
-      class="elements-list__item-link"
-      :class="{
-        'elements-list__item-link--selected': element.id === elementSelectedId,
-      }"
-      @click="$emit('select', element)"
-    >
-      <span class="elements-list__item-name">
-        <i :class="`${elementType.iconClass} elements-list__item-icon`"></i>
-        <span class="elements-list__item-name-text">{{
+  <li
+    :key="element.id"
+    class="elements-list-item"
+    :class="{
+      'elements-list-item--selected': element.id === elementSelectedId,
+    }"
+  >
+    <a class="elements-list-item__link" @click="$emit('select', element)">
+      <span class="elements-list-item__name">
+        <i :class="`${elementType.iconClass} elements-list-item__icon`"></i>
+        <span class="elements-list-item__name-text">{{
           elementType.getDisplayName(element, applicationContext)
         }}</span>
       </span>
@@ -33,7 +33,7 @@ export default {
     ElementsList: () =>
       import('@baserow/modules/builder/components/elements/ElementsList'),
   },
-  inject: ['builder', 'page', 'mode'],
+  inject: ['builder', 'mode'],
   props: {
     element: {
       type: Object,
@@ -55,8 +55,18 @@ export default {
     elementType() {
       return this.$registry.get('element', this.element.type)
     },
+    elementPage() {
+      // We use the page from the element itself
+      return this.$store.getters['page/getById'](
+        this.builder,
+        this.element.page_id
+      )
+    },
     children() {
-      return this.$store.getters['element/getChildren'](this.page, this.element)
+      return this.$store.getters['element/getChildren'](
+        this.elementPage,
+        this.element
+      )
     },
     /**
      * Responsible for returning elements to display in `ElementsList`.
@@ -76,7 +86,7 @@ export default {
     applicationContext() {
       return {
         builder: this.builder,
-        page: this.page,
+        page: this.elementPage,
         mode: this.mode,
         element: this.element,
       }
diff --git a/web-frontend/modules/builder/components/elements/baseComponents/ABLink.vue b/web-frontend/modules/builder/components/elements/baseComponents/ABLink.vue
index d5d7c8c12..6a81d6515 100644
--- a/web-frontend/modules/builder/components/elements/baseComponents/ABLink.vue
+++ b/web-frontend/modules/builder/components/elements/baseComponents/ABLink.vue
@@ -75,7 +75,9 @@ export default {
       }
       if (this.target === 'self' && this.url.startsWith('/')) {
         event.preventDefault()
-        this.$router.push(this.url)
+        if (this.$route.path !== this.url) {
+          this.$router.push(this.url)
+        }
       }
     },
   },
diff --git a/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue b/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
index db2696734..2b657c740 100644
--- a/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
+++ b/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
@@ -25,8 +25,13 @@
       </slot>
     </template>
     <template #empty-state>
-      <div class="ab-table__empty-message">
-        {{ emptyStateMessage }}
+      <div class="ab-table__empty-state">
+        <template v-if="contentLoading">
+          <div class="loading-spinner" />
+        </template>
+        <template v-else>
+          {{ $t('abTable.empty') }}
+        </template>
       </div>
     </template>
   </BaserowTable>
diff --git a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
index 185565291..596ce6f4e 100644
--- a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
+++ b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
@@ -20,7 +20,7 @@
 
 <script>
 export default {
-  inject: ['builder', 'page'],
+  inject: ['builder', 'currentPage'],
   props: {
     element: {
       type: Object,
@@ -33,7 +33,7 @@ export default {
     },
     dataSource() {
       return this.$store.getters['dataSource/getPagesDataSourceById'](
-        [this.page, this.sharedPage],
+        [this.currentPage, this.sharedPage],
         this.element.data_source_id
       )
     },
diff --git a/web-frontend/modules/builder/components/elements/components/ColumnElement.vue b/web-frontend/modules/builder/components/elements/components/ColumnElement.vue
index bc4e7fa81..3dfb3d8dc 100644
--- a/web-frontend/modules/builder/components/elements/components/ColumnElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/ColumnElement.vue
@@ -22,7 +22,7 @@
             v-if="mode === 'editing'"
             :element="childCurrent"
             :application-context-additions="applicationContextAdditions"
-            @move="move(childCurrent, $event)"
+            @move="$emit('move', $event)"
           ></ElementPreview>
           <PageElement
             v-else
@@ -35,21 +35,21 @@
       <AddElementZone
         v-else-if="
           mode === 'editing' &&
-          $hasPermission('builder.page.create_element', page, workspace.id)
+          $hasPermission(
+            'builder.page.create_element',
+            elementPage,
+            workspace.id
+          )
         "
+        :page="elementPage"
         @add-element="showAddElementModal(columnIndex)"
       />
     </div>
-    <AddElementModal
-      ref="addElementModal"
-      :page="page"
-      :element-types-allowed="elementType.childElementTypes(page, element)"
-    />
+    <AddElementModal ref="addElementModal" :page="elementPage" />
   </div>
 </template>
 
 <script>
-import { mapActions } from 'vuex'
 import _ from 'lodash'
 
 import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
@@ -58,7 +58,6 @@ import containerElement from '@baserow/modules/builder/mixins/containerElement'
 import PageElement from '@baserow/modules/builder/components/page/PageElement'
 import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
 import { VERTICAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
-import { notifyIf } from '@baserow/modules/core/utils/error'
 import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
 
 export default {
@@ -136,26 +135,12 @@ export default {
     this.dimensions.targetElement = this.$el.parentElement
   },
   methods: {
-    ...mapActions({
-      actionMoveElement: 'element/moveElement',
-    }),
     showAddElementModal(columnIndex) {
       this.$refs.addElementModal.show({
         placeInContainer: `${columnIndex}`,
         parentElementId: this.element.id,
       })
     },
-    async move(element, placement) {
-      try {
-        await this.actionMoveElement({
-          page: this.page,
-          element,
-          placement,
-        })
-      } catch (error) {
-        notifyIf(error)
-      }
-    },
   },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/elements/components/FormContainerElement.vue b/web-frontend/modules/builder/components/elements/components/FormContainerElement.vue
index 9c3352b5e..1c8b6091b 100644
--- a/web-frontend/modules/builder/components/elements/components/FormContainerElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/FormContainerElement.vue
@@ -4,14 +4,13 @@
       v-if="
         mode === 'editing' &&
         children.length === 0 &&
-        $hasPermission('builder.page.create_element', page, workspace.id)
+        $hasPermission('builder.page.create_element', currentPage, workspace.id)
       "
     >
       <AddElementZone @add-element="showAddElementModal"></AddElementZone>
       <AddElementModal
         ref="addElementModal"
-        :page="page"
-        :element-types-allowed="elementType.childElementTypes(page, element)"
+        :page="elementPage"
       ></AddElementModal>
     </div>
     <div v-else>
@@ -20,7 +19,7 @@
           v-if="mode === 'editing'"
           :key="child.id"
           :element="child"
-          @move="moveElement(child, $event)"
+          @move="$emit('move', $event)"
         />
         <PageElement
           v-else
@@ -42,15 +41,12 @@
 </template>
 
 <script>
-import { mapActions } from 'vuex'
-
 import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone.vue'
 import containerElement from '@baserow/modules/builder/mixins/containerElement'
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
 import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview.vue'
 import PageElement from '@baserow/modules/builder/components/page/PageElement.vue'
 import { ensureString } from '@baserow/modules/core/utils/validator'
-import { notifyIf } from '@baserow/modules/core/utils/error'
 
 export default {
   name: 'FormContainerElement',
@@ -64,7 +60,6 @@ export default {
   props: {
     /**
      * @type {Object}
-     * @property button_color - The submit button's color.
      * @property submit_button_label - The label of the submit button
      * @property reset_initial_values_post_submission - Whether to reset the form
      *  elements to their initial value or not, following a successful submission.
@@ -83,7 +78,7 @@ export default {
     },
     getFormElementDescendants() {
       const descendants = this.$store.getters['element/getDescendants'](
-        this.page,
+        this.elementPage,
         this.element
       )
       return descendants
@@ -107,7 +102,7 @@ export default {
             recordIndexPath
           )
           return this.$store.getters['formData/getElementInvalid'](
-            this.page,
+            this.elementPage,
             uniqueElementId
           )
         }
@@ -115,9 +110,6 @@ export default {
     },
   },
   methods: {
-    ...mapActions({
-      actionMoveElement: 'element/moveElement',
-    }),
     /*
      * Responsible for marking all form element descendents in this form container
      * as touched, or not touched, depending on what we're achieving in validation.
@@ -131,7 +123,7 @@ export default {
             recordIndexPath
           )
           this.$store.dispatch('formData/setElementTouched', {
-            page: this.page,
+            page: this.elementPage,
             wasTouched,
             uniqueElementId,
           })
@@ -169,7 +161,7 @@ export default {
               ),
             }
             this.$store.dispatch('formData/setFormData', {
-              page: this.page,
+              page: this.elementPage,
               payload,
               uniqueElementId,
             })
@@ -197,17 +189,6 @@ export default {
         parentElementId: this.element.id,
       })
     },
-    async moveElement(element, placement) {
-      try {
-        await this.actionMoveElement({
-          page: this.page,
-          element,
-          placement,
-        })
-      } catch (error) {
-        notifyIf(error)
-      }
-    },
   },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/elements/components/MultiPageContainerElement.vue b/web-frontend/modules/builder/components/elements/components/MultiPageContainerElement.vue
new file mode 100644
index 000000000..aefb79ad9
--- /dev/null
+++ b/web-frontend/modules/builder/components/elements/components/MultiPageContainerElement.vue
@@ -0,0 +1,82 @@
+<template>
+  <div>
+    <template
+      v-if="
+        mode === 'editing' &&
+        children.length === 0 &&
+        $hasPermission('builder.page.create_element', currentPage, workspace.id)
+      "
+    >
+      <AddElementZone @add-element="showAddElementModal"></AddElementZone>
+      <AddElementModal
+        ref="addElementModal"
+        :page="elementPage"
+      ></AddElementModal>
+    </template>
+    <template v-else>
+      <template v-for="child in children">
+        <ElementPreview
+          v-if="mode === 'editing'"
+          :key="child.id"
+          :element="child"
+          @move="$emit('move', $event)"
+        />
+        <PageElement
+          v-else
+          :key="`${child.id}else`"
+          :element="child"
+          :mode="mode"
+        />
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone.vue'
+import containerElement from '@baserow/modules/builder/mixins/containerElement'
+import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
+import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview.vue'
+import PageElement from '@baserow/modules/builder/components/page/PageElement.vue'
+import { ensureString } from '@baserow/modules/core/utils/validator'
+
+export default {
+  name: 'MultiPageContainerElement',
+  components: {
+    PageElement,
+    ElementPreview,
+    AddElementModal,
+    AddElementZone,
+  },
+  mixins: [containerElement],
+  props: {
+    /**
+     * @type {Object}
+     * @property page_position - [header|footer|left|right]
+     *   Position of this element on the page.
+     * @property behaviour - [scroll|fixed|sticky]
+     *   How this element follow the scroll of the page.
+     * @property shared_type - [all_pages|only_pages|except_pages] Type of share
+     * @property pages - List of pages the element is visible or excluded depending on
+     *   the share_type.
+     */
+    element: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    submitButtonLabelResolved() {
+      return ensureString(this.resolveFormula(this.element.submit_button_label))
+    },
+  },
+  methods: {
+    showAddElementModal() {
+      this.$refs.addElementModal.show({
+        placeInContainer: null,
+        parentElementId: this.element.id,
+      })
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/builder/components/elements/components/RepeatElement.vue b/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
index 2be484f84..ac8826e36 100644
--- a/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
@@ -34,7 +34,7 @@
                       index,
                     ],
                   }"
-                  @move="moveElement(child, $event)"
+                  @move="$emit('move', $event)"
                 />
                 <!-- Other iterations are not editable -->
                 <!-- Override the mode so that any children are in public mode -->
@@ -68,10 +68,7 @@
           ></AddElementZone>
           <AddElementModal
             ref="addElementModal"
-            :page="page"
-            :element-types-allowed="
-              elementType.childElementTypes(page, element)
-            "
+            :page="elementPage"
           ></AddElementModal>
         </template>
       </template>
@@ -92,10 +89,7 @@
           ></AddElementZone>
           <AddElementModal
             ref="addElementModal"
-            :page="page"
-            :element-types-allowed="
-              elementType.childElementTypes(page, element)
-            "
+            :page="elementPage"
           ></AddElementModal>
         </template>
         <!-- We have no contents, but we do have children in edit mode -->
@@ -106,7 +100,7 @@
               v-for="child in children"
               :key="child.id"
               :element="child"
-              @move="moveElement(child, $event)"
+              @move="$emit('move', $event)"
             />
           </template>
         </template>
@@ -127,7 +121,7 @@
 </template>
 
 <script>
-import { mapActions, mapGetters } from 'vuex'
+import { mapGetters } from 'vuex'
 
 import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
 import containerElement from '@baserow/modules/builder/mixins/containerElement'
@@ -135,7 +129,6 @@ import collectionElement from '@baserow/modules/builder/mixins/collectionElement
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
 import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
 import PageElement from '@baserow/modules/builder/components/page/PageElement'
-import { notifyIf } from '@baserow/modules/core/utils/error'
 import { ensureString } from '@baserow/modules/core/utils/validator'
 import { RepeatElementType } from '@baserow/modules/builder/elementTypes'
 import CollectionElementHeader from '@baserow/modules/builder/components/elements/components/CollectionElementHeader'
@@ -173,7 +166,7 @@ export default {
     },
     repeatElementIsNested() {
       return this.elementType.hasAncestorOfType(
-        this.page,
+        this.elementPage,
         this.element,
         RepeatElementType.getType()
       )
@@ -212,26 +205,12 @@ export default {
     },
   },
   methods: {
-    ...mapActions({
-      actionMoveElement: 'element/moveElement',
-    }),
     showAddElementModal() {
       this.$refs.addElementModal.show({
         placeInContainer: null,
         parentElementId: this.element.id,
       })
     },
-    async moveElement(element, placement) {
-      try {
-        await this.actionMoveElement({
-          page: this.page,
-          element,
-          placement,
-        })
-      } catch (error) {
-        notifyIf(error)
-      }
-    },
   },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/elements/components/collectionField/ButtonField.vue b/web-frontend/modules/builder/components/elements/components/collectionField/ButtonField.vue
index 854c97ffb..e58368c6d 100644
--- a/web-frontend/modules/builder/components/elements/components/collectionField/ButtonField.vue
+++ b/web-frontend/modules/builder/components/elements/components/collectionField/ButtonField.vue
@@ -31,7 +31,7 @@ export default {
       )
       const workflowActions = this.$store.getters[
         'workflowAction/getElementWorkflowActions'
-      ](this.page, this.element.id)
+      ](this.elementPage, this.element.id)
       return workflowActions
         .filter((wa) => wa.event === this.eventName)
         .some((workflowAction) =>
diff --git a/web-frontend/modules/builder/components/elements/components/forms/VisibilityForm.vue b/web-frontend/modules/builder/components/elements/components/forms/VisibilityForm.vue
index c45704ef7..f69fe8607 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/VisibilityForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/VisibilityForm.vue
@@ -31,7 +31,7 @@
 
           <div
             v-if="allowAllRolesExceptSelected || disallowAllRolesExceptSelected"
-            class="visibility-form__role-checkbox-container"
+            class="visibility-form__role-list"
           >
             <template v-if="loadingRoles">
               <div class="loading margin-bottom-1"></div>
@@ -40,7 +40,7 @@
               <div
                 v-for="roleName in allRoles"
                 :key="roleName"
-                class="visibility-form__role-checkbox-div"
+                class="visibility-form__role-checkbox"
               >
                 <Checkbox
                   :checked="isChecked(roleName)"
@@ -50,14 +50,11 @@
                 </Checkbox>
               </div>
 
-              <div class="visibility-form__role-links">
+              <div class="visibility-form__actions">
                 <a @click.prevent="selectAllRoles">
                   {{ $t('visibilityForm.rolesSelectAll') }}
                 </a>
-                <a
-                  class="visibility-form__role-links-deselect-all"
-                  @click.prevent="deselectAllRoles"
-                >
+                <a @click.prevent="deselectAllRoles">
                   {{ $t('visibilityForm.rolesDeselectAll') }}
                 </a>
               </div>
@@ -79,6 +76,7 @@
 
 <script>
 import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
+import elementForm from '@baserow/modules/builder/mixins/elementForm'
 
 import {
   VISIBILITY_ALL,
@@ -87,7 +85,7 @@ import {
 
 export default {
   name: 'VisibilityForm',
-  mixins: [visibilityForm],
+  mixins: [elementForm, visibilityForm],
   data() {
     return {
       values: {
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/ChoiceElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/ChoiceElementForm.vue
index 336377efb..740fa31d3 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/ChoiceElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/ChoiceElementForm.vue
@@ -228,7 +228,7 @@ export default {
     CHOICE_OPTION_TYPES: () => CHOICE_OPTION_TYPES,
     element() {
       return this.$store.getters['element/getElementById'](
-        this.page,
+        this.elementPage,
         this.values.id
       )
     },
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/MultiPageContainerElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/MultiPageContainerElementForm.vue
new file mode 100644
index 000000000..68a1fd50e
--- /dev/null
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/MultiPageContainerElementForm.vue
@@ -0,0 +1,131 @@
+<template>
+  <form @submit.prevent @keydown.enter.prevent>
+    <FormGroup
+      small-label
+      :label="$t('multiPageContainerElementForm.display')"
+      class="margin-bottom-2"
+      required
+    >
+      <Dropdown v-model="computedPageShareType" :show-search="false" small>
+        <DropdownItem
+          v-for="item in pageShareTypes"
+          :key="item.value"
+          :name="item.label"
+          :value="item.value"
+        >
+          {{ item.label }}
+        </DropdownItem>
+      </Dropdown>
+      <template v-if="values.share_type !== 'all'">
+        <div class="multi-page-container-element-form__page-list">
+          <div
+            v-for="page in pages"
+            :key="page.id"
+            class="multi-page-container-element-form__page-checkbox"
+          >
+            <Checkbox
+              :checked="values.pages.includes(page.id)"
+              @input="togglePage(page)"
+            >
+              {{ page.name }}
+            </Checkbox>
+          </div>
+
+          <div class="multi-page-container-element-form__actions">
+            <a @click.prevent="selectAll">
+              {{ $t('multiPageContainerElementForm.selectAll') }}
+            </a>
+            <a @click.prevent="deselectAll">
+              {{ $t('multiPageContainerElementForm.deselectAll') }}
+            </a>
+          </div>
+        </div>
+      </template>
+    </FormGroup>
+  </form>
+</template>
+
+<script>
+import elementForm from '@baserow/modules/builder/mixins/elementForm'
+import { SHARE_TYPES } from '@baserow/modules/builder/enums'
+
+export default {
+  name: 'MultiPageContainerElementForm',
+  mixins: [elementForm],
+  data() {
+    return {
+      values: {
+        share_type: '',
+        pages: [],
+        styles: {},
+      },
+      allowedValues: ['share_type', 'pages', 'styles'],
+    }
+  },
+  computed: {
+    computedPageShareType: {
+      get() {
+        return this.values.share_type
+      },
+      set(newValue) {
+        if (
+          [SHARE_TYPES.ONLY, SHARE_TYPES.EXCEPT].includes(newValue) &&
+          newValue !== this.values.share_type
+        ) {
+          if (![SHARE_TYPES.ALL, undefined].includes(this.values.share_type)) {
+            // We want to invert the page selection if we change from except <-> only
+            this.values.pages = this.pageIds.filter(
+              (id) => !this.values.pages.includes(id)
+            )
+          } else {
+            // Otherwise we want to select all or none.
+            this.values.pages =
+              newValue === SHARE_TYPES.ONLY ? [...this.pageIds] : []
+          }
+        }
+
+        this.values.share_type = newValue
+      },
+    },
+    pageShareTypes() {
+      return [
+        {
+          label: this.$t('pageShareType.all'),
+          value: SHARE_TYPES.ALL,
+        },
+        {
+          label: this.$t('pageShareType.only'),
+          value: SHARE_TYPES.ONLY,
+        },
+        {
+          label: this.$t('pageShareType.except'),
+          value: SHARE_TYPES.EXCEPT,
+        },
+      ]
+    },
+    pages() {
+      return this.$store.getters['page/getVisiblePages'](this.builder)
+    },
+    pageIds() {
+      return this.pages.map(({ id }) => id)
+    },
+  },
+  methods: {
+    togglePage(page) {
+      if (!this.values.pages.includes(page.id)) {
+        this.values.pages.push(page.id)
+      } else {
+        this.values.pages = this.values.pages.filter(
+          (pageId) => pageId !== page.id
+        )
+      }
+    },
+    selectAll() {
+      this.values.pages = this.pageIds
+    },
+    deselectAll() {
+      this.values.pages = []
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
index bec26096e..adaec1a5d 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
@@ -15,8 +15,8 @@
       <DataSourceDropdown
         v-model="values.data_source_id"
         small
-        :data-sources="listDataSources"
-        :page="page"
+        :shared-data-sources="listSharedDataSources"
+        :local-data-sources="listLocalDataSources"
       >
         <template #chooseValueState>
           {{ $t('recordSelectorElementForm.noDataSourceMessage') }}
@@ -186,10 +186,18 @@ export default {
   },
   computed: {
     // For now, RecordSelector only supports data sources that return arrays
-    listDataSources() {
-      return this.dataSources.filter(
+    listLocalDataSources() {
+      if (this.localDataSources === null) {
+        return null
+      }
+      return this.localDataSources.filter(
+        (dataSource) =>
+          this.$registry.get('service', dataSource.type).returnsList
+      )
+    },
+    listSharedDataSources() {
+      return this.sharedDataSources.filter(
         (dataSource) =>
-          dataSource.type &&
           this.$registry.get('service', dataSource.type).returnsList
       )
     },
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/RepeatElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/RepeatElementForm.vue
index d9fc825e5..dbda96f46 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/RepeatElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/RepeatElementForm.vue
@@ -10,8 +10,8 @@
       <DataSourceDropdown
         v-model="values.data_source_id"
         small
-        :data-sources="dataSources"
-        :page="page"
+        :shared-data-sources="sharedDataSources"
+        :local-data-sources="localDataSources"
       >
         <template #chooseValueState>
           {{ $t('collectionElementForm.noDataSourceMessage') }}
@@ -150,7 +150,6 @@
 <script>
 import _ from 'lodash'
 import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
-import elementForm from '@baserow/modules/builder/mixins/elementForm'
 import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
 import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
 import { mapActions, mapGetters } from 'vuex'
@@ -170,7 +169,7 @@ export default {
     InjectedFormulaInput,
     ServiceSchemaPropertySelector,
   },
-  mixins: [elementForm, collectionElementForm],
+  mixins: [collectionElementForm],
   inject: ['applicationContext'],
   data() {
     return {
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/TableElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/TableElementForm.vue
index 1acfed0f6..87efe5b15 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/TableElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/TableElementForm.vue
@@ -17,8 +17,8 @@
       <DataSourceDropdown
         v-model="computedDataSourceId"
         small
-        :data-sources="dataSources"
-        :page="page"
+        :shared-data-sources="sharedDataSources"
+        :local-data-sources="localDataSources"
       >
         <template #chooseValueState>
           {{ $t('collectionElementForm.noDataSourceMessage') }}
@@ -259,7 +259,6 @@ import {
   minValue,
   maxValue,
 } from 'vuelidate/lib/validators'
-import elementForm from '@baserow/modules/builder/mixins/elementForm'
 import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
 import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
 import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
@@ -279,7 +278,7 @@ export default {
     DeviceSelector,
     CustomStyle,
   },
-  mixins: [elementForm, collectionElementForm],
+  mixins: [collectionElementForm],
   data() {
     return {
       allowedValues: [
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/settings/PropertyOptionForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/settings/PropertyOptionForm.vue
index 42abaa59e..66f4ba246 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/settings/PropertyOptionForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/settings/PropertyOptionForm.vue
@@ -60,7 +60,6 @@ export default {
   name: 'PropertyOptionForm',
   components: { BaserowTable },
   mixins: [form],
-  inject: ['page'],
   props: {
     dataSource: {
       type: Object,
diff --git a/web-frontend/modules/builder/components/event/Event.vue b/web-frontend/modules/builder/components/event/Event.vue
index f9125a028..19ca028e1 100644
--- a/web-frontend/modules/builder/components/event/Event.vue
+++ b/web-frontend/modules/builder/components/event/Event.vue
@@ -78,7 +78,7 @@ export default {
   name: 'Event',
   components: { WorkflowAction },
   mixins: [applicationContext],
-  inject: ['workspace', 'builder', 'page'],
+  inject: ['workspace', 'builder', 'elementPage'],
   props: {
     event: {
       type: Event,
@@ -128,7 +128,7 @@ export default {
       this.addingAction = true
       try {
         await this.actionCreateWorkflowAction({
-          page: this.page,
+          page: this.elementPage,
           workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
           eventType: this.event.name,
           configuration: {
@@ -143,7 +143,7 @@ export default {
     async deleteWorkflowAction(workflowAction) {
       try {
         await this.actionDeleteWorkflowAction({
-          page: this.page,
+          page: this.elementPage,
           workflowAction,
         })
       } catch (error) {
@@ -153,7 +153,7 @@ export default {
     async orderWorkflowActions(order) {
       try {
         await this.actionOrderWorkflowActions({
-          page: this.page,
+          page: this.elementPage,
           element: this.element,
           order,
         })
diff --git a/web-frontend/modules/builder/components/page/CreatePageModal.vue b/web-frontend/modules/builder/components/page/CreatePageModal.vue
index eb80716b8..1390df462 100644
--- a/web-frontend/modules/builder/components/page/CreatePageModal.vue
+++ b/web-frontend/modules/builder/components/page/CreatePageModal.vue
@@ -29,7 +29,7 @@ export default {
   mixins: [modal],
   provide() {
     return {
-      page: null,
+      currentPage: null,
       builder: this.builder,
       workspace: this.workspace,
     }
diff --git a/web-frontend/modules/builder/components/page/PageContent.vue b/web-frontend/modules/builder/components/page/PageContent.vue
index 9d25c1fb0..06985588f 100644
--- a/web-frontend/modules/builder/components/page/PageContent.vue
+++ b/web-frontend/modules/builder/components/page/PageContent.vue
@@ -1,5 +1,14 @@
 <template>
   <ThemeProvider class="page">
+    <PageElement
+      v-for="element in headerElements"
+      :key="element.id"
+      :element="element"
+      :mode="mode"
+      :application-context-additions="{
+        recordIndexPath: [],
+      }"
+    />
     <PageElement
       v-for="element in elements"
       :key="element.id"
@@ -9,6 +18,15 @@
         recordIndexPath: [],
       }"
     />
+    <PageElement
+      v-for="element in footerElements"
+      :key="element.id"
+      :element="element"
+      :mode="mode"
+      :application-context-additions="{
+        recordIndexPath: [],
+      }"
+    />
   </ThemeProvider>
 </template>
 
@@ -17,16 +35,13 @@ import PageElement from '@baserow/modules/builder/components/page/PageElement'
 import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
 import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
 import _ from 'lodash'
+import { PAGE_PLACES } from '@baserow/modules/builder/enums'
 
 export default {
   components: { ThemeProvider, PageElement },
   mixins: [dimensionMixin],
   inject: ['builder', 'mode'],
   props: {
-    page: {
-      type: Object,
-      required: true,
-    },
     path: {
       type: String,
       required: true,
@@ -39,6 +54,26 @@ export default {
       type: Array,
       required: true,
     },
+    sharedElements: {
+      type: Array,
+      required: true,
+    },
+  },
+  computed: {
+    headerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.HEADER
+      )
+    },
+    footerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.FOOTER
+      )
+    },
   },
   watch: {
     'dimensions.width': {
diff --git a/web-frontend/modules/builder/components/page/PageElement.vue b/web-frontend/modules/builder/components/page/PageElement.vue
index bd249fc09..6b024e946 100644
--- a/web-frontend/modules/builder/components/page/PageElement.vue
+++ b/web-frontend/modules/builder/components/page/PageElement.vue
@@ -17,12 +17,14 @@
     <div class="element__inner-wrapper">
       <component
         :is="component"
+        :key="element._.uid"
         :element="element"
-        :children="children"
         :application-context-additions="{
           element,
+          page: elementPage,
         }"
         class="element"
+        v-on="$listeners"
       />
     </div>
   </div>
@@ -49,9 +51,9 @@ import { mapGetters } from 'vuex'
 export default {
   name: 'PageElement',
   mixins: [applicationContextMixin],
-  inject: ['builder', 'page', 'mode'],
+  inject: ['builder', 'mode', 'currentPage'],
   provide() {
-    return { mode: this.elementMode }
+    return { mode: this.elementMode, elementPage: this.elementPage }
   },
   props: {
     element: {
@@ -79,16 +81,23 @@ export default {
         this.elementMode === 'editing' ? 'editComponent' : 'component'
       return elementType[componentName]
     },
-    children() {
-      return this.$store.getters['element/getChildren'](this.page, this.element)
-    },
     ...mapGetters({
       loggedUser: 'userSourceUser/getUser',
     }),
+    elementPage() {
+      // We use the page from the element itself
+      return this.$store.getters['page/getById'](
+        this.builder,
+        this.element.page_id
+      )
+    },
+    elementType() {
+      return this.$registry.get('element', this.element.type)
+    },
     isVisible() {
       const elementType = this.$registry.get('element', this.element.type)
       const isInError = elementType.isInError({
-        page: this.page,
+        page: this.elementPage,
         element: this.element,
         builder: this.builder,
       })
@@ -97,6 +106,15 @@ export default {
         return false
       }
 
+      if (
+        !this.elementType.isVisible({
+          element: this.element,
+          currentPage: this.currentPage,
+        })
+      ) {
+        return false
+      }
+
       const isAuthenticated = this.$store.getters[
         'userSourceUser/isAuthenticated'
       ](this.builder)
diff --git a/web-frontend/modules/builder/components/page/PagePreview.vue b/web-frontend/modules/builder/components/page/PagePreview.vue
index 854b0a87b..d1933b3c8 100644
--- a/web-frontend/modules/builder/components/page/PagePreview.vue
+++ b/web-frontend/modules/builder/components/page/PagePreview.vue
@@ -1,10 +1,10 @@
 <template>
-  <ThemeProvider
+  <div
     class="page-preview__wrapper"
     :class="`page-preview__wrapper--${deviceType.type}`"
     @click.self="actionSelectElement({ element: null })"
   >
-    <PreviewNavigationBar :page="page" :style="{ maxWidth }" />
+    <PreviewNavigationBar :page="currentPage" :style="{ maxWidth }" />
     <div ref="preview" class="page-preview" :style="{ 'max-width': maxWidth }">
       <div
         ref="previewScaled"
@@ -12,36 +12,101 @@
         tabindex="0"
         @keydown="handleKeyDown"
       >
-        <CallToAction
-          v-if="!elements.length"
-          class="page-preview__empty"
-          icon="baserow-icon-plus"
-          icon-color="neutral"
-          icon-size="large"
-          icon-rounded
-          @click="$refs.addElementModal.show()"
-        >
-          {{ $t('pagePreview.emptyMessage') }}
-        </CallToAction>
-        <div v-else class="page">
-          <ElementPreview
-            v-for="(element, index) in elements"
-            :key="element.id"
-            is-root-element
-            :element="element"
-            :is-first-element="index === 0"
-            :is-last-element="index === elements.length - 1"
-            :is-copying="copyingElementIndex === index"
-            :application-context-additions="{
-              recordIndexPath: [],
-            }"
-            @move="moveElement($event)"
-          />
-        </div>
+        <ThemeProvider class="page">
+          <template v-if="headerElements.length !== 0">
+            <header
+              class="page__header"
+              :class="{
+                'page__header--element-selected':
+                  pageSectionWithSelectedElement === 'header',
+              }"
+            >
+              <ElementPreview
+                v-for="(element, index) in headerElements"
+                :key="element.id"
+                :element="element"
+                :is-first-element="index === 0"
+                :is-copying="copyingElementIndex === index"
+                :application-context-additions="{
+                  recordIndexPath: [],
+                }"
+                @move="moveElement($event)"
+              />
+            </header>
+            <div class="page-preview__separator">
+              <span class="page-preview__separator-label">
+                {{ $t('pagePreview.header') }}
+              </span>
+            </div>
+          </template>
+          <template v-if="elements.length === 0">
+            <CallToAction
+              class="page-preview__empty"
+              icon="baserow-icon-plus"
+              icon-color="neutral"
+              icon-size="large"
+              icon-rounded
+              @click="$refs.addElementModal.show()"
+            >
+              {{ $t('pagePreview.emptyMessage') }}
+            </CallToAction>
+          </template>
+          <template v-else>
+            <div
+              class="page__content"
+              :class="{
+                'page__content--element-selected':
+                  pageSectionWithSelectedElement === 'content',
+              }"
+            >
+              <ElementPreview
+                v-for="(element, index) in elements"
+                :key="element.id"
+                :element="element"
+                :is-first-element="index === 0 && headerElements.length === 0"
+                :is-copying="copyingElementIndex === index"
+                :application-context-additions="{
+                  recordIndexPath: [],
+                }"
+                @move="moveElement($event)"
+              />
+            </div>
+          </template>
+          <template v-if="footerElements.length !== 0">
+            <div class="page-preview__separator">
+              <span class="page-preview__separator-label">
+                {{ $t('pagePreview.footer') }}
+              </span>
+            </div>
+            <footer
+              class="page__footer"
+              :class="{
+                'page__footer--element-selected':
+                  pageSectionWithSelectedElement === 'footer',
+              }"
+            >
+              <ElementPreview
+                v-for="(element, index) in footerElements"
+                :key="element.id"
+                :element="element"
+                :is-first-element="
+                  index === 0 &&
+                  headerElements.length === 0 &&
+                  elements.length === 0
+                "
+                :is-copying="copyingElementIndex === index"
+                :application-context-additions="{
+                  recordIndexPath: [],
+                }"
+                @move="moveElement($event)"
+              />
+            </footer>
+          </template>
+        </ThemeProvider>
       </div>
-      <AddElementModal ref="addElementModal" :page="page" />
+      <AddElementModal ref="addElementModal" :page="currentPage" />
     </div>
-  </ThemeProvider>
+  </div>
 </template>
 
 <script>
@@ -50,7 +115,7 @@ import { mapActions, mapGetters } from 'vuex'
 import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
 import { notifyIf } from '@baserow/modules/core/utils/error'
 import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
-import { PLACEMENTS } from '@baserow/modules/builder/enums'
+import { DIRECTIONS, PAGE_PLACES } from '@baserow/modules/builder/enums'
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
 import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
 
@@ -62,7 +127,7 @@ export default {
     ElementPreview,
     PreviewNavigationBar,
   },
-  inject: ['page', 'workspace'],
+  inject: ['builder', 'currentPage', 'workspace'],
   data() {
     return {
       // The element that is currently being copied
@@ -73,7 +138,7 @@ export default {
     }
   },
   computed: {
-    PLACEMENTS: () => PLACEMENTS,
+    DIRECTIONS: () => DIRECTIONS,
     ...mapGetters({
       deviceTypeSelected: 'page/getDeviceTypeSelected',
       elementSelected: 'element/getSelected',
@@ -81,11 +146,84 @@ export default {
       getClosestSiblingElement: 'element/getClosestSiblingElement',
     }),
     elements() {
-      return this.$store.getters['element/getRootElements'](this.page)
+      return this.$store.getters['element/getRootElements'](this.currentPage)
+    },
+    sharedPage() {
+      return this.$store.getters['page/getSharedPage'](this.builder)
+    },
+    sharedElements() {
+      return this.$store.getters['element/getRootElements'](this.sharedPage)
+    },
+    headerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.HEADER
+      )
+    },
+    footerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.FOOTER
+      )
     },
     elementSelectedId() {
       return this.elementSelected?.id
     },
+    elementSelectedType() {
+      if (!this.elementSelected) {
+        return null
+      }
+      return this.$registry.get('element', this.elementSelected.type)
+    },
+    pageSectionWithSelectedElement() {
+      if (!this.elementSelected) {
+        return null
+      }
+      if (this.elementSelected.page_id === this.currentPage.id) {
+        return PAGE_PLACES.CONTENT
+      }
+      const ancestorWithPagePlace = this.$store.getters['element/getAncestors'](
+        this.elementSelectedPage,
+        this.elementSelected,
+        {
+          includeSelf: true,
+          predicate: (parentElement) => {
+            return (
+              this.$registry
+                .get('element', parentElement.type)
+                .getPagePlace() !== PAGE_PLACES.CONTENT
+            )
+          },
+        }
+      )[0]
+
+      return this.$registry
+        .get('element', ancestorWithPagePlace.type)
+        .getPagePlace()
+    },
+    elementsAround() {
+      if (!this.elementSelected) {
+        return null
+      }
+      return this.elementSelectedType.getElementsAround({
+        builder: this.builder,
+        page: this.currentPage,
+        element: this.elementSelected,
+        withSharedPage: true,
+      })
+    },
+    elementSelectedPage() {
+      if (this.elementSelected) {
+        // We use the page from the element itself
+        return this.$store.getters['page/getById'](
+          this.builder,
+          this.elementSelected.page_id
+        )
+      }
+      return null
+    },
     deviceType() {
       return this.deviceTypeSelected
         ? this.$registry.get('device', this.deviceTypeSelected)
@@ -101,14 +239,14 @@ export default {
         return null
       }
       return this.$store.getters['element/getElementById'](
-        this.page,
+        this.elementSelectedPage,
         this.elementSelected.parent_element_id
       )
     },
     canCreateElement() {
       return this.$hasPermission(
         'builder.page.create_element',
-        this.page,
+        this.currentPage,
         this.workspace.id
       )
     },
@@ -156,9 +294,8 @@ export default {
     ...mapActions({
       actionDuplicateElement: 'element/duplicate',
       actionDeleteElement: 'element/delete',
-      actionMoveElement: 'element/moveElement',
       actionSelectElement: 'element/select',
-      actionSelectNextElement: 'element/selectNextElement',
+      actionMoveElement: 'element/move',
     }),
     preventScrollIfFocused(e) {
       if (this.$refs.previewScaled === document.activeElement) {
@@ -199,62 +336,57 @@ export default {
       previewScaled.style.width = `${currentWidth / scale}px`
       previewScaled.style.height = `${currentHeight / scale}px`
     },
+    async moveElement({ element, direction }) {
+      if (
+        !this.$hasPermission(
+          'builder.page.element.update',
+          element,
+          this.workspace.id
+        )
+      ) {
+        return
+      }
 
-    async moveElement(placement) {
+      const elementPage = this.$store.getters['page/getById'](
+        this.builder,
+        element.page_id
+      )
+
+      const elementType = this.$registry.get('element', element.type)
+
+      const nextPlaces = elementType.getNextPlaces({
+        builder: this.builder,
+        page: this.currentPage,
+        element,
+      })
+      if (nextPlaces[direction]) {
+        try {
+          await this.actionMoveElement({
+            page: elementPage,
+            elementId: element.id,
+            ...nextPlaces[direction],
+          })
+        } catch (error) {
+          notifyIf(error)
+        }
+      }
+    },
+    async moveSelectedElement(direction) {
       if (!this.elementSelected?.id || !this.canUpdateSelectedElement) {
         return
       }
-
-      const elementType = this.$registry.get(
-        'element',
-        this.elementSelected.type
-      )
-      const placementsDisabled = elementType.getPlacementsDisabled(
-        this.page,
-        this.elementSelected
-      )
-
-      if (placementsDisabled.includes(placement)) {
-        return
-      }
-
-      try {
-        await this.actionMoveElement({
-          page: this.page,
-          element: this.elementSelected,
-          placement,
-        })
-        await this.actionSelectElement({ element: this.elementSelected })
-      } catch (error) {
-        notifyIf(error)
-      }
+      await this.moveElement({
+        element: this.elementSelected,
+        direction,
+      })
     },
-    async selectNextElement(placement) {
+    async moveSelection(direction) {
       if (!this.elementSelected?.id) {
         return
       }
-
-      const elementType = this.$registry.get(
-        'element',
-        this.elementSelected.type
-      )
-      const placementsDisabled = elementType.getPlacementsDisabled(
-        this.page,
-        this.elementSelected
-      )
-
-      if (placementsDisabled.includes(placement)) {
-        return
-      }
-
-      try {
-        await this.actionSelectNextElement({
-          page: this.page,
-          element: this.elementSelected,
-          placement,
-        })
-      } catch (error) {
-        notifyIf(error)
+      const nextElement = this.elementsAround[direction]
+      if (nextElement) {
+        await this.actionSelectElement({ element: nextElement })
       }
     },
     async duplicateElement() {
@@ -265,7 +397,7 @@ export default {
       this.isDuplicating = true
       try {
         await this.actionDuplicateElement({
-          page: this.page,
+          page: this.elementSelectedPage,
           elementId: this.elementSelected.id,
         })
       } catch (error) {
@@ -278,12 +410,15 @@ export default {
         return
       }
       try {
-        const siblingElementToSelect = this.getClosestSiblingElement(
-          this.page,
-          this.elementSelected
-        )
+        const siblingElementToSelect =
+          this.elementsAround[DIRECTIONS.AFTER] ||
+          this.elementsAround[DIRECTIONS.BEFORE] ||
+          this.elementsAround[DIRECTIONS.LEFT] ||
+          this.elementsAround[DIRECTIONS.RIGHT] ||
+          this.parentOfElementSelected
+
         await this.actionDeleteElement({
-          page: this.page,
+          page: this.elementSelectedPage,
           elementId: this.elementSelected.id,
         })
         if (siblingElementToSelect?.id) {
@@ -299,7 +434,10 @@ export default {
       }
     },
     selectChildElement() {
-      const children = this.getChildren(this.page, this.elementSelected)
+      const children = this.getChildren(
+        this.elementSelectedPage,
+        this.elementSelected
+      )
       if (children.length) {
         this.actionSelectElement({ element: children[0] })
       }
@@ -310,30 +448,30 @@ export default {
       switch (e.key) {
         case 'ArrowUp':
           if (alternateAction) {
-            this.moveElement(PLACEMENTS.BEFORE)
+            this.moveSelectedElement(DIRECTIONS.BEFORE)
           } else {
-            this.selectNextElement(PLACEMENTS.BEFORE)
+            this.moveSelection(DIRECTIONS.BEFORE)
           }
           break
         case 'ArrowDown':
           if (alternateAction) {
-            this.moveElement(PLACEMENTS.AFTER)
+            this.moveSelectedElement(DIRECTIONS.AFTER)
           } else {
-            this.selectNextElement(PLACEMENTS.AFTER)
+            this.moveSelection(DIRECTIONS.AFTER)
           }
           break
         case 'ArrowLeft':
           if (alternateAction) {
-            this.moveElement(PLACEMENTS.LEFT)
+            this.moveSelectedElement(DIRECTIONS.LEFT)
           } else {
-            this.selectNextElement(PLACEMENTS.LEFT)
+            this.moveSelection(DIRECTIONS.LEFT)
           }
           break
         case 'ArrowRight':
           if (alternateAction) {
-            this.moveElement(PLACEMENTS.RIGHT)
+            this.moveSelectedElement(DIRECTIONS.RIGHT)
           } else {
-            this.selectNextElement(PLACEMENTS.RIGHT)
+            this.moveSelection(DIRECTIONS.RIGHT)
           }
           break
         case 'Backspace':
diff --git a/web-frontend/modules/builder/components/page/PageTemplate.vue b/web-frontend/modules/builder/components/page/PageTemplate.vue
index 147ba75ea..bbba222ca 100644
--- a/web-frontend/modules/builder/components/page/PageTemplate.vue
+++ b/web-frontend/modules/builder/components/page/PageTemplate.vue
@@ -1,9 +1,9 @@
 <template>
   <PageTemplateContent
-    v-if="!loading && workspace && page && builder"
+    v-if="!loading && workspace && currentPage && builder"
     :workspace="workspace"
     :builder="builder"
-    :page="page"
+    :current-page="currentPage"
     :mode="mode"
   />
   <PageSkeleton v-else />
@@ -32,7 +32,7 @@ export default {
     return {
       workspace: null,
       builder: null,
-      page: null,
+      currentPage: null,
       mode,
       loading: true,
     }
@@ -104,7 +104,7 @@ export default {
         )
 
         this.builder = builder
-        this.page = page
+        this.currentPage = page
         this.workspace = builder.workspace
       } catch (e) {
         // In case of a network error we want to fail hard.
diff --git a/web-frontend/modules/builder/components/page/PageTemplateContent.vue b/web-frontend/modules/builder/components/page/PageTemplateContent.vue
index bc2f2ce9b..1959b2923 100644
--- a/web-frontend/modules/builder/components/page/PageTemplateContent.vue
+++ b/web-frontend/modules/builder/components/page/PageTemplateContent.vue
@@ -1,6 +1,6 @@
 <template>
-  <div v-if="page" :key="page.id" class="page-template">
-    <PageHeader :page="page" />
+  <div v-if="currentPage" :key="currentPage.id" class="page-template">
+    <PageHeader />
     <div class="layout__col-2-2 page-editor__content">
       <div :style="{ width: `calc(100% - ${panelWidth}px)` }">
         <PagePreview />
@@ -32,10 +32,13 @@ export default {
     return {
       workspace: this.workspace,
       builder: this.builder,
-      page: this.page,
+      currentPage: this.currentPage,
       mode,
       formulaComponent: ApplicationBuilderFormulaInput,
-      applicationContext: { builder: this.builder, page: this.page, mode },
+      applicationContext: {
+        builder: this.builder,
+        mode,
+      },
     }
   },
   props: {
@@ -47,7 +50,7 @@ export default {
       type: Object,
       required: true,
     },
-    page: {
+    currentPage: {
       type: Object,
       required: true,
     },
@@ -63,7 +66,7 @@ export default {
     applicationContext() {
       return {
         builder: this.builder,
-        page: this.page,
+        page: this.currentPage,
         mode,
       }
     },
@@ -76,7 +79,9 @@ export default {
       )
     },
     dataSources() {
-      return this.$store.getters['dataSource/getPageDataSources'](this.page)
+      return this.$store.getters['dataSource/getPageDataSources'](
+        this.currentPage
+      )
     },
     dispatchContext() {
       return DataProviderType.getAllDataSourceDispatchContext(
@@ -96,7 +101,7 @@ export default {
           this.$store.dispatch(
             'dataSourceContent/debouncedFetchPageDataSourceContent',
             {
-              page: this.page,
+              page: this.currentPage,
               data: newDispatchContext,
               mode: this.mode,
             }
diff --git a/web-frontend/modules/builder/components/page/UserSourceUsersContext.vue b/web-frontend/modules/builder/components/page/UserSourceUsersContext.vue
index 912db8468..de99c2e8c 100644
--- a/web-frontend/modules/builder/components/page/UserSourceUsersContext.vue
+++ b/web-frontend/modules/builder/components/page/UserSourceUsersContext.vue
@@ -79,7 +79,7 @@ import { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
 export default {
   name: 'UserSourceUsersContext',
   mixins: [context],
-  inject: ['page', 'builder'],
+  inject: ['builder'],
   data() {
     return {
       state: null,
diff --git a/web-frontend/modules/builder/components/page/header/ElementsContext.vue b/web-frontend/modules/builder/components/page/header/ElementsContext.vue
index c9efac12f..95fc7f4cc 100644
--- a/web-frontend/modules/builder/components/page/header/ElementsContext.vue
+++ b/web-frontend/modules/builder/components/page/header/ElementsContext.vue
@@ -10,24 +10,49 @@
         ref="search"
         v-model="search"
         type="text"
-        class="elements-list__search-input"
+        class="elements-context__search-input"
         :placeholder="$t('elementsContext.searchPlaceholder')"
       />
     </div>
-    <ElementsList
-      v-if="elementsVisible"
-      :elements="rootElements"
-      :filtered-search-elements="filteredSearchElements"
-      @select="selectElement($event)"
-    />
-    <div v-else class="context__description">
-      {{ $t('elementsContext.noElements') }}
+    <div class="elements-context__elements">
+      <ElementsList
+        v-if="headerElementsVisible"
+        :elements="headerElements"
+        :filtered-search-elements="filteredHeaderElements"
+        @select="selectElement($event)"
+      />
+      <ElementsList
+        v-if="contentElementsVisible"
+        :elements="rootElements"
+        :filtered-search-elements="filteredContentElements"
+        @select="selectElement($event)"
+      />
+      <div
+        v-if="!contentElementsVisible && !isSearching"
+        class="elements-list elements-list--empty"
+      >
+        {{ $t('elementsContext.noPageElements') }}
+      </div>
+      <ElementsList
+        v-if="footerElementsVisible"
+        :elements="footerElements"
+        :filtered-search-elements="filteredFooterElements"
+        @select="selectElement($event)"
+      />
+      <div
+        v-if="!elementsVisible && isSearching"
+        class="elements-list elements-list--empty"
+      >
+        {{ $t('elementsContext.noElements') }}
+      </div>
     </div>
     <div
-      v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
-      class="elements-list__footer"
+      v-if="
+        $hasPermission('builder.page.create_element', currentPage, workspace.id)
+      "
+      class="elements-context__footer"
     >
-      <div class="elements-list__footer-create">
+      <div class="elements-context__footer-create">
         <AddElementButton
           :class="{
             'margin-top-1': !elementsVisible,
@@ -37,9 +62,11 @@
       </div>
     </div>
     <AddElementModal
-      v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
+      v-if="
+        $hasPermission('builder.page.create_element', currentPage, workspace.id)
+      "
       ref="addElementModal"
-      :page="page"
+      :page="currentPage"
       @element-added="onElementAdded"
     />
   </Context>
@@ -52,27 +79,110 @@ import AddElementButton from '@baserow/modules/builder/components/elements/AddEl
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
 import { mapActions } from 'vuex'
 import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
+import { PAGE_PLACES } from '@baserow/modules/builder/enums'
 
 export default {
   name: 'ElementsContext',
   components: { AddElementModal, AddElementButton, ElementsList },
   mixins: [context],
-  inject: ['workspace', 'page', 'builder', 'mode'],
+  inject: ['workspace', 'currentPage', 'builder', 'mode'],
   data() {
     return {
       search: null,
-      addingElementType: null,
     }
   },
   computed: {
+    isSearching() {
+      return Boolean(this.search)
+    },
     elementsVisible() {
       return (
-        (this.search && this.filteredSearchElements.length) ||
+        (this.search &&
+          (this.filteredContentElements.length ||
+            this.filteredHeaderElements.length ||
+            this.filteredFooterElements.length)) ||
+        (!this.search &&
+          (this.rootElements.length ||
+            this.headerElements.length ||
+            this.footerElements.length))
+      )
+    },
+    contentElementsVisible() {
+      return (
+        (this.search && this.filteredContentElements.length) ||
         (!this.search && this.rootElements.length)
       )
     },
+    headerElementsVisible() {
+      return (
+        (this.search && this.filteredHeaderElements.length) ||
+        (!this.search && this.headerElements.length)
+      )
+    },
+    footerElementsVisible() {
+      return (
+        (this.search && this.filteredFooterElements.length) ||
+        (!this.search && this.footerElements.length)
+      )
+    },
     rootElements() {
-      return this.$store.getters['element/getRootElements'](this.page)
+      return this.$store.getters['element/getRootElements'](this.currentPage)
+    },
+    sharedPage() {
+      return this.$store.getters['page/getSharedPage'](this.builder)
+    },
+    sharedElements() {
+      return this.$store.getters['element/getRootElements'](this.sharedPage)
+    },
+    headerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.HEADER
+      )
+    },
+    footerElements() {
+      return this.sharedElements.filter(
+        (element) =>
+          this.$registry.get('element', element.type).getPagePlace() ===
+          PAGE_PLACES.FOOTER
+      )
+    },
+    filteredContentElements() {
+      return this.filterElements(this.rootElements, this.currentPage)
+    },
+    filteredHeaderElements() {
+      return this.filterElements(this.headerElements, this.sharedPage)
+    },
+    filteredFooterElements() {
+      return this.filterElements(this.footerElements, this.sharedPage)
+    },
+  },
+  methods: {
+    ...mapActions({
+      actionSelectElement: 'element/select',
+    }),
+    /*
+     * Given an element, this method will return the corpus we want to
+     * search against when a user enters a search query. Should we want
+     * to search both the 'display name' and the element type name, this
+     * method can be easily adapted to combine the two and return it.
+     */
+    getElementCorpus(element, page) {
+      const elementType = this.$registry.get('element', element.type)
+      return elementType.getDisplayName(element, {
+        builder: this.builder,
+        page,
+        mode: this.mode,
+        element,
+      })
+    },
+    onElementAdded() {
+      this.hide()
+    },
+    selectElement(element) {
+      this.actionSelectElement({ element })
+      this.hide()
     },
     /*
      * When a user searches for elements in the list, this computed method
@@ -102,34 +212,30 @@ export default {
      *         - Repeat
      *             - Image
      */
-    filteredSearchElements() {
+    filterElements(elements, page) {
       let filteredToElementIds = []
-      if (
-        this.search === '' ||
-        this.search === null ||
-        this.search === undefined
-      ) {
+      if (!this.search) {
         // If there's no search query, then there are no
         // elements to narrow the results down to.
         return filteredToElementIds
       }
 
       // Iterate over all the root-level elements.
-      this.rootElements.forEach((rootElement) => {
+      elements.forEach((element) => {
         // Find this element's descendants and loop over them.
         const descendants = this.$store.getters['element/getDescendants'](
-          this.page,
-          rootElement
+          page,
+          element
         )
         descendants.forEach((descendant) => {
           // Build this descendant's corpus (for now, display name only)
           // and check if it matches the search query.
-          const descendantCorpus = this.getElementCorpus(descendant)
+          const descendantCorpus = this.getElementCorpus(descendant, page)
           if (isSubstringOfStrings([descendantCorpus], this.search)) {
             // The descendant matches. We need to include *this* element,
             // and all its *ancestors* in our list of narrowed results.
             const ascendants = this.$store.getters['element/getAncestors'](
-              this.page,
+              page,
               descendant
             )
             filteredToElementIds.push(descendant.id)
@@ -141,42 +247,15 @@ export default {
 
         // Test of the root element itself matches the search query.
         // if it does, it gets included in the narrowed results too.
-        const rootCorpus = this.getElementCorpus(rootElement)
+        const rootCorpus = this.getElementCorpus(element, page)
         if (isSubstringOfStrings([rootCorpus], this.search)) {
           // The root element matches.
-          filteredToElementIds.push(rootElement.id)
+          filteredToElementIds.push(element.id)
         }
       })
       filteredToElementIds = [...new Set(filteredToElementIds)]
       return filteredToElementIds
     },
-  },
-  methods: {
-    ...mapActions({
-      actionSelectElement: 'element/select',
-    }),
-    /*
-     * Given an element, this method will return the corpus we want to
-     * search against when a user enters a search query. Should we want
-     * to search both the 'display name' and the element type name, this
-     * method can be easily adapted to combine the two and return it.
-     */
-    getElementCorpus(element) {
-      const elementType = this.$registry.get('element', element.type)
-      return elementType.getDisplayName(element, {
-        builder: this.builder,
-        page: this.page,
-        mode: this.mode,
-        element,
-      })
-    },
-    onElementAdded() {
-      this.hide()
-    },
-    selectElement(element) {
-      this.actionSelectElement({ element })
-      this.hide()
-    },
     shown() {
       this.search = null
       this.$nextTick(() => {
diff --git a/web-frontend/modules/builder/components/page/header/PageActions.vue b/web-frontend/modules/builder/components/page/header/PageActions.vue
index 2a7e82fa4..b45621ecd 100644
--- a/web-frontend/modules/builder/components/page/header/PageActions.vue
+++ b/web-frontend/modules/builder/components/page/header/PageActions.vue
@@ -2,7 +2,7 @@
   <ul class="header__filter">
     <template v-for="actionType in pageActionTypes">
       <li
-        v-if="actionType.isActive({ page, workspace })"
+        v-if="actionType.isActive({ page: currentPage, workspace })"
         :key="actionType.getType()"
         class="header__filter-item header__filter-item--right"
       >
@@ -14,7 +14,7 @@
               component: $refs[`component_${actionType.type}`][0],
               button: $refs[`button_${actionType.type}`][0],
               builder: builder,
-              page: page,
+              page: currentPage,
             })
           "
         >
@@ -29,7 +29,7 @@
           :is="actionType.component"
           :ref="`component_${actionType.type}`"
           :builder="builder"
-          :page="page"
+          :page="currentPage"
         />
       </li>
     </template>
@@ -39,7 +39,7 @@
 <script>
 export default {
   name: 'PageActions',
-  inject: ['workspace', 'builder', 'page'],
+  inject: ['workspace', 'builder', 'currentPage'],
   computed: {
     pageActionTypes() {
       return Object.values(this.$registry.getOrderedList('pageAction'))
diff --git a/web-frontend/modules/builder/components/page/header/PageHeader.vue b/web-frontend/modules/builder/components/page/header/PageHeader.vue
index b1c062736..b89763006 100644
--- a/web-frontend/modules/builder/components/page/header/PageHeader.vue
+++ b/web-frontend/modules/builder/components/page/header/PageHeader.vue
@@ -22,12 +22,6 @@ export default {
     DeviceSelector,
     PageActions,
   },
-  props: {
-    page: {
-      type: Object,
-      required: true,
-    },
-  },
   computed: {
     ...mapGetters({ deviceTypeSelected: 'page/getDeviceTypeSelected' }),
     deviceTypes() {
diff --git a/web-frontend/modules/builder/components/page/header/PageHeaderMenuItems.vue b/web-frontend/modules/builder/components/page/header/PageHeaderMenuItems.vue
index b93e4a843..ee926943c 100644
--- a/web-frontend/modules/builder/components/page/header/PageHeaderMenuItems.vue
+++ b/web-frontend/modules/builder/components/page/header/PageHeaderMenuItems.vue
@@ -23,7 +23,7 @@
         :is="itemType.component"
         :ref="`component_${itemType.type}`"
         :data-item-type="itemType.type"
-        :page="page"
+        :page="currentPage"
       />
     </li>
   </ul>
@@ -32,7 +32,7 @@
 <script>
 export default {
   name: 'PageHeaderMenuItems',
-  inject: ['page'],
+  inject: ['currentPage'],
   computed: {
     pageHeaderItemTypes() {
       return this.$registry.getOrderedList('pageHeaderItem')
diff --git a/web-frontend/modules/builder/components/page/settings/PageSettings.vue b/web-frontend/modules/builder/components/page/settings/PageSettings.vue
index 1c30794be..84baca588 100644
--- a/web-frontend/modules/builder/components/page/settings/PageSettings.vue
+++ b/web-frontend/modules/builder/components/page/settings/PageSettings.vue
@@ -8,12 +8,12 @@
     </Alert>
     <PageSettingsForm
       :builder="builder"
-      :page="page"
-      :default-values="page"
+      :page="currentPage"
+      :default-values="currentPage"
       @submitted="updatePage"
     >
       <div
-        v-if="$hasPermission('builder.page.update', page, workspace.id)"
+        v-if="$hasPermission('builder.page.update', currentPage, workspace.id)"
         class="actions actions--right"
       >
         <Button size="large" :loading="loading" :disabled="loading">
@@ -34,7 +34,7 @@ export default {
   name: 'PageSettings',
   components: { PageSettingsForm },
   mixins: [error],
-  inject: ['builder', 'page', 'workspace'],
+  inject: ['builder', 'currentPage', 'workspace'],
   data() {
     return {
       loading: false,
@@ -50,7 +50,7 @@ export default {
       try {
         await this.actionUpdatePage({
           builder: this.builder,
-          page: this.page,
+          page: this.currentPage,
           values: {
             name,
             path,
@@ -60,7 +60,7 @@ export default {
         await Promise.all(
           pathPrams.map(({ name, type }) =>
             this.$store.dispatch('pageParameter/setParameter', {
-              page: this.page,
+              page: this.currentPage,
               name,
               value: defaultValueForParameterType(type),
             })
diff --git a/web-frontend/modules/builder/components/page/settings/PageSettingsForm.vue b/web-frontend/modules/builder/components/page/settings/PageSettingsForm.vue
index 214fd20e6..f7eb0a06e 100644
--- a/web-frontend/modules/builder/components/page/settings/PageSettingsForm.vue
+++ b/web-frontend/modules/builder/components/page/settings/PageSettingsForm.vue
@@ -62,8 +62,13 @@ export default {
     PageSettingsNameFormElement,
   },
   mixins: [form],
-  inject: ['workspace', 'builder', 'page'],
+  inject: ['workspace', 'builder'],
   props: {
+    page: {
+      type: Object,
+      required: false,
+      default: null,
+    },
     isCreation: {
       type: Boolean,
       required: false,
diff --git a/web-frontend/modules/builder/components/page/settings/PageVisibilityForm.vue b/web-frontend/modules/builder/components/page/settings/PageVisibilityForm.vue
index b12523f50..267626d7a 100644
--- a/web-frontend/modules/builder/components/page/settings/PageVisibilityForm.vue
+++ b/web-frontend/modules/builder/components/page/settings/PageVisibilityForm.vue
@@ -76,7 +76,7 @@
                     allowAllRolesExceptSelected ||
                     disallowAllRolesExceptSelected
                   "
-                  class="visibility-form__role-checkbox-container"
+                  class="visibility-form__role-list"
                 >
                   <template v-if="loadingRoles">
                     <div class="loading margin-bottom-1"></div>
@@ -85,7 +85,7 @@
                     <div
                       v-for="roleName in allRoles"
                       :key="roleName"
-                      class="visibility-form__role-checkbox-div"
+                      class="visibility-form__role-checkbox"
                     >
                       <Checkbox
                         :checked="isChecked(roleName)"
@@ -95,14 +95,11 @@
                       </Checkbox>
                     </div>
 
-                    <div class="visibility-form__role-links">
+                    <div class="visibility-form__actions">
                       <a @click.prevent="selectAllRoles">
                         {{ $t('visibilityForm.rolesSelectAll') }}
                       </a>
-                      <a
-                        class="visibility-form__role-links-deselect-all"
-                        @click.prevent="deselectAllRoles"
-                      >
+                      <a @click.prevent="deselectAllRoles">
                         {{ $t('visibilityForm.rolesDeselectAll') }}
                       </a>
                     </div>
@@ -120,12 +117,13 @@
 <script>
 import { StoreItemLookupError } from '@baserow/modules/core/errors'
 import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
+import form from '@baserow/modules/core/mixins/form'
 
 import { VISIBILITY_LOGGED_IN } from '@baserow/modules/builder/constants'
 
 export default {
   name: 'PageVisibilityForm',
-  mixins: [visibilityForm],
+  mixins: [form, visibilityForm],
   data() {
     return {
       values: {
diff --git a/web-frontend/modules/builder/components/page/settings/PageVisibilitySettings.vue b/web-frontend/modules/builder/components/page/settings/PageVisibilitySettings.vue
index 0ed6a8c63..6f2c24376 100644
--- a/web-frontend/modules/builder/components/page/settings/PageVisibilitySettings.vue
+++ b/web-frontend/modules/builder/components/page/settings/PageVisibilitySettings.vue
@@ -4,7 +4,7 @@
 
     <div>
       <PageVisibilityForm
-        :default-values="page"
+        :default-values="currentPage"
         @values-changed="updatePageVisibility"
       />
     </div>
@@ -20,7 +20,7 @@ export default {
   name: 'PageVisibilitySettings',
   components: { PageVisibilityForm },
   mixins: [error],
-  inject: ['builder', 'page', 'workspace'],
+  inject: ['builder', 'currentPage', 'workspace'],
   data() {
     return {}
   },
@@ -31,7 +31,7 @@ export default {
       try {
         await this.actionUpdatePage({
           builder: this.builder,
-          page: this.page,
+          page: this.currentPage,
           values,
         })
       } catch (error) {
diff --git a/web-frontend/modules/builder/components/page/sidePanels/EventsSidePanel.vue b/web-frontend/modules/builder/components/page/sidePanels/EventsSidePanel.vue
index 109c8263d..286dd07bf 100644
--- a/web-frontend/modules/builder/components/page/sidePanels/EventsSidePanel.vue
+++ b/web-frontend/modules/builder/components/page/sidePanels/EventsSidePanel.vue
@@ -22,13 +22,8 @@ export default {
   name: 'EventsSidePanel',
   components: { Event },
   mixins: [elementSidePanel],
-  inject: ['applicationContext'],
   provide() {
     return {
-      applicationContext: {
-        ...this.applicationContext,
-        element: this.element,
-      },
       dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS,
     }
   },
@@ -38,7 +33,7 @@ export default {
     },
     workflowActions() {
       return this.$store.getters['workflowAction/getElementWorkflowActions'](
-        this.page,
+        this.elementPage,
         this.element.id
       )
     },
diff --git a/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue b/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue
index ffc02bd6f..de83d4549 100644
--- a/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue
+++ b/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue
@@ -16,13 +16,8 @@ import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
 export default {
   name: 'GeneralSidePanel',
   mixins: [elementSidePanel],
-  inject: ['applicationContext'],
   provide() {
     return {
-      applicationContext: {
-        ...this.applicationContext,
-        element: this.element,
-      },
       dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS,
     }
   },
diff --git a/web-frontend/modules/builder/components/settings/GeneralSettings.vue b/web-frontend/modules/builder/components/settings/GeneralSettings.vue
index 2ef7ae596..be50c0052 100644
--- a/web-frontend/modules/builder/components/settings/GeneralSettings.vue
+++ b/web-frontend/modules/builder/components/settings/GeneralSettings.vue
@@ -30,7 +30,6 @@ export default {
   provide() {
     return {
       builder: this.builder,
-      page: null,
       mode: null,
     }
   },
diff --git a/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue b/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue
index 431cbfcf3..3be1ffede 100644
--- a/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue
+++ b/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue
@@ -209,7 +209,6 @@ export default {
       try {
         await this.actionUpdateUserSource({
           application: this.builder,
-          page: this.page,
           userSourceId: this.editedUserSource.id,
           values: clone(newValues),
         })
diff --git a/web-frontend/modules/builder/components/workflowAction/RefreshDataSourceWorkflowActionForm.vue b/web-frontend/modules/builder/components/workflowAction/RefreshDataSourceWorkflowActionForm.vue
index 4d4927b47..8466e2013 100644
--- a/web-frontend/modules/builder/components/workflowAction/RefreshDataSourceWorkflowActionForm.vue
+++ b/web-frontend/modules/builder/components/workflowAction/RefreshDataSourceWorkflowActionForm.vue
@@ -7,14 +7,13 @@
       class="margin-bottom-2"
     >
       <div class="control__elements">
-        <Dropdown v-model="values.data_source_id" :show-search="false">
-          <DropdownItem
-            v-for="dataSource in dataSources"
-            :key="dataSource.id"
-            :name="dataSource.name"
-            :value="dataSource.id"
-          />
-        </Dropdown>
+        <DataSourceDropdown
+          v-model="values.data_source_id"
+          small
+          :shared-data-sources="sharedDataSources"
+          :local-data-sources="localDataSources"
+        >
+        </DataSourceDropdown>
       </div>
     </FormGroup>
   </form>
@@ -22,9 +21,11 @@
 
 <script>
 import elementForm from '@baserow/modules/builder/mixins/elementForm'
+import DataSourceDropdown from '@baserow/modules/builder/components/dataSource/DataSourceDropdown'
 
 export default {
   name: 'RefreshDataSourceWorkflowActionForm',
+  components: { DataSourceDropdown },
   mixins: [elementForm],
   props: {
     workflowAction: {
@@ -42,8 +43,39 @@ export default {
     }
   },
   computed: {
-    dataSources() {
-      return this.$store.getters['dataSource/getPageDataSources'](this.page)
+    sharedPage() {
+      return this.$store.getters['page/getSharedPage'](this.builder)
+    },
+    /**
+     * Returns all data sources that are available not on shared page.
+     * @returns {Array} - The data sources the page designer can choose from.
+     */
+    localDataSources() {
+      if (this.elementPage.id === this.sharedPage.id) {
+        // If the element is on the shared page they are no local page but only
+        // shared page.
+        return null
+      } else {
+        return this.$store.getters['dataSource/getPagesDataSources']([
+          this.elementPage,
+        ]).filter((dataSource) => dataSource.type)
+      }
+    },
+    /**
+     * Returns the shared data sources.
+     * @returns {Array} - The shared data sources the page designer can choose from.
+     */
+    sharedDataSources() {
+      // We keep only data sources that have a type and a schema.
+      return this.$store.getters['dataSource/getPagesDataSources']([
+        this.sharedPage,
+      ]).filter(
+        (dataSource) =>
+          dataSource.type &&
+          this.$registry
+            .get('service', dataSource.type)
+            .getDataSchema(dataSource)
+      )
     },
   },
 }
diff --git a/web-frontend/modules/builder/elementTypeMixins.js b/web-frontend/modules/builder/elementTypeMixins.js
new file mode 100644
index 000000000..8000952f5
--- /dev/null
+++ b/web-frontend/modules/builder/elementTypeMixins.js
@@ -0,0 +1,364 @@
+import { ELEMENT_EVENTS, SHARE_TYPES } from '@baserow/modules/builder/enums'
+
+export const ContainerElementTypeMixin = (Base) =>
+  class extends Base {
+    isContainerElement = true
+
+    get elementTypesAll() {
+      return Object.values(this.app.$registry.getAll('element'))
+    }
+
+    /**
+     * Returns an array of style types that are not allowed as children of this element.
+     * @returns {Array}
+     */
+    get childStylesForbidden() {
+      return []
+    }
+
+    get defaultPlaceInContainer() {
+      throw new Error('Not implemented')
+    }
+
+    /**
+     * Returns the default value when creating a child element to this container.
+     * @param {Object} page The current page object
+     * @param {Object} values The values of the to be created element
+     * @returns the default values for this element.
+     */
+    getDefaultChildValues(page, values) {
+      // By default, an element inside a container should have no left and right padding
+      return { style_padding_left: 0, style_padding_right: 0 }
+    }
+
+    /**
+     * A Container element without any child elements is invalid. Return true
+     * if there are no children, otherwise return false.
+     */
+    isInError({ page, element }) {
+      const children = this.app.store.getters['element/getChildren'](
+        page,
+        element
+      )
+
+      return !children.length
+    }
+  }
+
+export const CollectionElementTypeMixin = (Base) =>
+  class extends Base {
+    isCollectionElement = true
+
+    /**
+     * A helper function responsible for returning this collection element's
+     * schema properties.
+     */
+    getSchemaProperties(dataSource) {
+      const serviceType = this.app.$registry.get('service', dataSource.type)
+      const schema = serviceType.getDataSchema(dataSource)
+      if (!schema) {
+        return []
+      }
+      return schema.type === 'array'
+        ? schema.items.properties
+        : schema.properties
+    }
+
+    /**
+     * Given a schema property name, is responsible for finding the matching
+     * property option in the element. If it doesn't exist, then we return
+     * an empty object, and it won't be included in the adhoc header.
+     * @param {object} element - the element we want to extract options from.
+     * @param {string} schemaProperty - the schema property name to check.
+     * @returns {object} - the matching property option, or an empty object.
+     */
+    getPropertyOptionsByProperty(element, schemaProperty) {
+      return (
+        element.property_options.find((option) => {
+          return option.schema_property === schemaProperty
+        }) || {}
+      )
+    }
+
+    /**
+     * Responsible for iterating over the schema's properties, filtering
+     * the results down to the properties which are `filterable`, `sortable`,
+     * and `searchable`, and then returning the property value.
+     * @param {string} option - the `filterable`, `sortable` or `searchable`
+     *  property option. If the value is `true` then the property will be
+     *  included in the adhoc header component.
+     * @param {object} element - the element we want to extract options from.
+     * @param {object} dataSource - the dataSource used by `element`.
+     * @returns {array} - an array of schema properties which are present
+     *  in the element's property options where `option` = `true`.
+     */
+    getPropertyOptionByType(option, element, dataSource) {
+      const schemaProperties = dataSource
+        ? this.getSchemaProperties(dataSource)
+        : []
+      return Object.entries(schemaProperties)
+        .filter(
+          ([schemaProperty, _]) =>
+            this.getPropertyOptionsByProperty(element, schemaProperty)[
+              option
+            ] || false
+        )
+        .map(([_, property]) => property)
+    }
+
+    /**
+     * An array of properties within this element which have been flagged
+     * as filterable by the page designer.
+     */
+    adhocFilterableProperties(element, dataSource) {
+      return this.getPropertyOptionByType('filterable', element, dataSource)
+    }
+
+    /**
+     * An array of properties within this element which have been flagged
+     * as sortable by the page designer.
+     */
+    adhocSortableProperties(element, dataSource) {
+      return this.getPropertyOptionByType('sortable', element, dataSource)
+    }
+
+    /**
+     * An array of properties within this element which have been flagged
+     * as searchable by the page designer.
+     */
+    adhocSearchableProperties(element, dataSource) {
+      return this.getPropertyOptionByType('searchable', element, dataSource)
+    }
+
+    /**
+     * By default collection element will load their content at loading time
+     * but if you don't want that you can return false here.
+     */
+    get fetchAtLoad() {
+      return true
+    }
+
+    hasCollectionAncestor(page, element) {
+      return this.app.store.getters['element/getAncestors'](page, element).some(
+        ({ type }) => {
+          const ancestorType = this.app.$registry.get('element', type)
+          return ancestorType.isCollectionElement
+        }
+      )
+    }
+
+    /**
+     * A simple check to return whether this collection element has a
+     * "source of data" (i.e. a data source, or a schema property).
+     * Should not be used as an "in error" or validation check, use
+     * `isInError` for this purpose as it is more thorough.
+     * @param element - The element we want to check for a source of data.
+     * @returns {Boolean} - Whether the element has a source of data.
+     */
+    hasSourceOfData(element) {
+      return Boolean(element.data_source_id || element.schema_property)
+    }
+
+    /**
+     * Collection elements by default will have three permutations of display names:
+     *
+     * 1. If no data source exists, on `element` or its ancestors, then:
+     *   - "Repeat" is returned.
+     * 2. If a data source is found, and `element` has no `schema_property`, then:
+     *   - "Repeat {dataSourceName}" is returned.
+     * 3. If a data source is found, `element` has a `schema_property`, and the integration is Baserow, then:
+     *   - "Repeat {schemaPropertyTitle} ({fieldTypeName})" is returned
+     * 4. If a data source is found, `element` has a `schema_property`, and the integration isn't Baserow, then:
+     *   - "Repeat {schemaPropertyTitle}" is returned
+     * @param element - The element we want to get a display name for.
+     * @param page - The page the element belongs to.
+     * @returns {string} - The display name for the element.
+     */
+    getDisplayName(element, { page, builder }) {
+      let suffix = ''
+
+      const collectionAncestors = this.app.store.getters[
+        'element/getAncestors'
+      ](page, element, {
+        predicate: (ancestor) =>
+          this.app.$registry.get('element', ancestor.type)
+            .isCollectionElement && ancestor.data_source_id !== null,
+      })
+
+      // If the collection element has ancestors, pluck out the first one, which
+      // will have a data source. Otherwise, use `element`, as this element is
+      // the root level element.
+      const collectionElement = collectionAncestors.length
+        ? collectionAncestors[0]
+        : element
+
+      // If we find a collection ancestor which has a data source, we'll
+      // use the data source's name as part of the display name.
+      if (collectionElement?.data_source_id) {
+        const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+        const dataSource = this.app.store.getters[
+          'dataSource/getPagesDataSourceById'
+        ]([page, sharedPage], collectionElement?.data_source_id)
+        suffix = dataSource ? dataSource.name : ''
+
+        // If we have a data source, and the element has a schema property,
+        // we'll find the property within the data source's schema and pluck
+        // out the title property.
+        if (element.schema_property) {
+          // Find the schema properties. They'll be in different places,
+          // depending on whether this is a list or single row data source.
+          const schemaProperties =
+            dataSource.schema.type === 'array'
+              ? dataSource.schema?.items?.properties
+              : dataSource.schema.properties
+          const schemaField = schemaProperties[element.schema_property]
+          // Only Local/Remote Baserow table schemas will have `original_type`,
+          // which is the `FieldType`. If we find it, we can use it to display
+          // what kind of field type was used.
+          suffix = schemaField?.title || element.schema_property
+          if (schemaField.original_type) {
+            const fieldType = this.app.$registry.get(
+              'field',
+              schemaField.original_type
+            )
+            suffix = `${suffix} (${fieldType.getName()})`
+          }
+        }
+      }
+
+      return suffix ? `${this.name} - ${suffix}` : this.name
+    }
+
+    /**
+     * When a data source is modified or destroyed, we need to ensure that
+     * our collection elements respond accordingly.
+     *
+     * If the data source has been removed, we want to remove it from the
+     * collection element, and then clear its contents from the store.
+     *
+     * If the data source has been updated, we want to trigger a content reset.
+     *
+     * @param event - `ELEMENT_EVENTS.DATA_SOURCE_REMOVED` if a data source
+     *  has been destroyed, or `ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE` if
+     *  it's been updated.
+     * @param params - Context data which the element type can use.
+     */
+    async onElementEvent(event, { builder, element, dataSourceId }) {
+      const page = this.app.store.getters['page/getById'](
+        builder,
+        element.page_id
+      )
+      if (event === ELEMENT_EVENTS.DATA_SOURCE_REMOVED) {
+        if (element.data_source_id === dataSourceId) {
+          // Remove the data_source_id
+          await this.app.store.dispatch('element/forceUpdate', {
+            page,
+            element,
+            values: { data_source_id: null },
+          })
+          // Empty the element content
+          await this.app.store.dispatch('elementContent/clearElementContent', {
+            element,
+          })
+        }
+      }
+      if (event === ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE) {
+        if (element.data_source_id === dataSourceId) {
+          await this.app.store.dispatch(
+            'elementContent/triggerElementContentReset',
+            {
+              element,
+            }
+          )
+        }
+      }
+    }
+
+    /**
+     * A collection element is in error if:
+     *
+     * - No parent (including self) collection elements have a valid data_source_id.
+     * - The parent with the valid data_source_id points to a data_source
+     *   that !returnsList and `schema_property` is blank.
+     * - It is nested in another collection element, and we don't have a `schema_property`.
+     * @param {Object} page - The page the repeat element belongs to.
+     * @param {Object} element - The repeat element
+     * @param {Object} builder - The builder
+     * @returns {Boolean} - Whether the element is in error.
+     */
+    isInError({ page, element, builder }) {
+      // We get all parents with a valid data_source_id
+      const collectionAncestorsWithDataSource = this.app.store.getters[
+        'element/getAncestors'
+      ](page, element, {
+        predicate: (ancestor) =>
+          this.app.$registry.get('element', ancestor.type)
+            .isCollectionElement && ancestor.data_source_id,
+        includeSelf: true,
+      })
+
+      // No parent with a data_source_id means we are in error
+      if (collectionAncestorsWithDataSource.length === 0) {
+        return true
+      }
+
+      // We consider the closest parent collection element with a data_source_id
+      // The closest parent might be the current element itself
+      const parentWithDataSource = collectionAncestorsWithDataSource.at(-1)
+
+      // We now check if the parent element configuration is correct.
+      const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+      const dataSource = this.app.store.getters[
+        'dataSource/getPagesDataSourceById'
+      ]([page, sharedPage], parentWithDataSource.data_source_id)
+
+      // The data source is missing. May be it has been removed.
+      if (!dataSource) {
+        return true
+      }
+
+      const serviceType = this.app.$registry.get('service', dataSource.type)
+
+      // If the data source type doesn't return a list, we should have a schema_property
+      if (!serviceType.returnsList && !parentWithDataSource.schema_property) {
+        return true
+      }
+
+      // If the current element is not the one with the data source it should have
+      // a schema_property
+      if (parentWithDataSource.id !== element.id && !element.schema_property) {
+        return true
+      }
+
+      return super.isInError({ page, element, builder })
+    }
+  }
+
+export const MultiPageElementTypeMixin = (Base) =>
+  class extends Base {
+    isMultiPageElement = true
+
+    get onSharedPage() {
+      return true
+    }
+
+    isVisible({ element, currentPage }) {
+      if (!super.isVisible({ element, currentPage })) {
+        return false
+      }
+      switch (element.share_type) {
+        case SHARE_TYPES.ALL:
+          return true
+        case SHARE_TYPES.ONLY:
+          return element.pages.includes(currentPage.id)
+        case SHARE_TYPES.EXCEPT:
+          return !element.pages.includes(currentPage.id)
+        default:
+          return false
+      }
+    }
+
+    get childStylesForbidden() {
+      return ['style_width']
+    }
+  }
diff --git a/web-frontend/modules/builder/elementTypes.js b/web-frontend/modules/builder/elementTypes.js
index b56e81dc4..0506a2905 100644
--- a/web-frontend/modules/builder/elementTypes.js
+++ b/web-frontend/modules/builder/elementTypes.js
@@ -21,15 +21,14 @@ import {
 import {
   CHOICE_OPTION_TYPES,
   DATE_FORMATS,
-  ELEMENT_EVENTS,
-  PLACEMENTS,
   TIME_FORMATS,
   IMAGE_SOURCE_TYPES,
   IFRAME_SOURCE_TYPES,
+  DIRECTIONS,
+  PAGE_PLACES,
 } from '@baserow/modules/builder/enums'
 import ColumnElement from '@baserow/modules/builder/components/elements/components/ColumnElement'
 import ColumnElementForm from '@baserow/modules/builder/components/elements/components/forms/general/ColumnElementForm'
-import _ from 'lodash'
 import DefaultStyleForm from '@baserow/modules/builder/components/elements/components/forms/style/DefaultStyleForm'
 import ButtonElement from '@baserow/modules/builder/components/elements/components/ButtonElement'
 import ButtonElementForm from '@baserow/modules/builder/components/elements/components/forms/general/ButtonElementForm'
@@ -47,11 +46,18 @@ import IFrameElementForm from '@baserow/modules/builder/components/elements/comp
 import RepeatElement from '@baserow/modules/builder/components/elements/components/RepeatElement'
 import RepeatElementForm from '@baserow/modules/builder/components/elements/components/forms/general/RepeatElementForm'
 import RecordSelectorElement from '@baserow/modules/builder/components/elements/components/RecordSelectorElement.vue'
+import RecordSelectorElementForm from '@baserow/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm'
+import MultiPageContainerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/MultiPageContainerElementForm'
+import MultiPageContainerElement from '@baserow/modules/builder/components/elements/components/MultiPageContainerElement'
+import DateTimePickerElement from '@baserow/modules/builder/components/elements/components/DateTimePickerElement'
+import DateTimePickerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/DateTimePickerElementForm'
 import { pathParametersInError } from '@baserow/modules/builder/utils/params'
+import {
+  ContainerElementTypeMixin,
+  CollectionElementTypeMixin,
+  MultiPageElementTypeMixin,
+} from '@baserow/modules/builder/elementTypeMixins'
 import { isNumeric, isValidEmail } from '@baserow/modules/core/utils/string'
-import RecordSelectorElementForm from '@baserow/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue'
-import DateTimePickerElement from '@baserow/modules/builder/components/elements/components/DateTimePickerElement.vue'
-import DateTimePickerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/DateTimePickerElementForm.vue'
 import { FormattedDate, FormattedDateTime } from '@baserow/modules/builder/date'
 
 export class ElementType extends Registerable {
@@ -105,6 +111,71 @@ export class ElementType extends Registerable {
     ]
   }
 
+  /**
+   * Returns the current place of the given element.
+   *
+   * @param {Object} element
+   * @returns a PAGE_PLACES enum
+   */
+  getPagePlace() {
+    return PAGE_PLACES.CONTENT
+  }
+
+  /**
+   * Returns the reason why this element type is disallowed for the given location.
+   * @param {Object} builder the current builder object
+   * @param {Object} page the current page
+   * @param {Object} parentElement the parent container element in which we want to
+   *   add the element in if any.
+   * @param {Object} beforeElement the element before which we want to add the element.
+   * @param {Object} placeInContainer The place in the container if we are in a
+   *   container
+   * @param {Object} pagePlace the page place we want to add the element to.
+   * @returns null if the element type is allowed or a string explaining the reason why
+   *   the element type is not allowed at the given location.
+   */
+  isDisallowedReason({
+    builder,
+    page: destinationPage,
+    parentElement,
+    beforeElement,
+    placeInContainer,
+    pagePlace,
+  }) {
+    if (!parentElement) {
+      const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+
+      if (pagePlace === PAGE_PLACES.HEADER) {
+        if (beforeElement && beforeElement.page_id === sharedPage.id) {
+          // It's not allowed to add these elements as root inside header before
+          // another multi page element
+          return this.app.i18n.t('elementType.notAllowedLocation')
+        }
+      }
+
+      if (pagePlace === PAGE_PLACES.FOOTER) {
+        if (!beforeElement) {
+          // Not allowed as last child of footer
+          return this.app.i18n.t('elementType.notAllowedLocation')
+        } else {
+          const footerElements = this.app.store.getters[
+            'element/getRootElements'
+          ](sharedPage).filter(
+            (element) =>
+              this.app.$registry.get('element', element.type).getPagePlace() ===
+              PAGE_PLACES.FOOTER
+          )
+          if (beforeElement.id !== footerElements[0].id) {
+            // It's not allowed to add these elements as root inside footer after
+            // another multi page element
+            return this.app.i18n.t('elementType.notAllowedLocation')
+          }
+        }
+      }
+    }
+    return null
+  }
+
   get styles() {
     return this.stylesAll
   }
@@ -128,6 +199,16 @@ export class ElementType extends Registerable {
     return this.getEvents(element).find((event) => event.name === name)
   }
 
+  /**
+   * Should return whether this element is visible.
+   * @param {Object} element the element to check
+   * @param {Object} currentPage the current displayed page
+   * @returns
+   */
+  isVisible({ element, currentPage }) {
+    return true
+  }
+
   /**
    * Returns whether the element configuration is valid or not.
    * @param {object} param An object containing the page, element, and builder
@@ -243,185 +324,299 @@ export class ElementType extends Registerable {
   afterUpdate(element, page) {}
 
   /**
-   * Move a component in the same place.
-   * @param {Object} page - The page the element belongs to
-   * @param {Object} element - The element to move
-   * @param {String} placement - The direction of the move
+   * Returns the places for this element. The places are the location where we can place
+   * a child element. Only collection elements can have places.
+   *
+   * @param {Object} element
+   * @returns an array of allowed places for the given collection element.
    */
-  async moveElementInSamePlace(page, element, placement) {
-    let beforeElementId = null
-
-    switch (placement) {
-      case PLACEMENTS.BEFORE: {
-        const previousElement = this.app.store.getters[
-          'element/getPreviousElement'
-        ](page, element)
-
-        beforeElementId = previousElement ? previousElement.id : null
-        break
-      }
-      case PLACEMENTS.AFTER: {
-        const nextElement = this.app.store.getters['element/getNextElement'](
-          page,
-          element
-        )
-
-        if (nextElement) {
-          const nextNextElement = this.app.store.getters[
-            'element/getNextElement'
-          ](page, nextElement)
-          beforeElementId = nextNextElement ? nextNextElement.id : null
-        }
-        break
-      }
-    }
-
-    await this.app.store.dispatch('element/move', {
-      page,
-      elementId: element.id,
-      beforeElementId,
-      parentElementId: element.parent_element_id
-        ? element.parent_element_id
-        : null,
-      placeInContainer: element.place_in_container,
-    })
+  getElementPlaces(element) {
+    return []
   }
 
   /**
-   * Move an element according to the new placement.
-   * @param {Object} page - The page the element belongs to
-   * @param {Object} element - The element to move
-   * @param {String} placement - The direction of the move
+   * Returns the places if we move an element in the four directions. Used when we want
+   * to now if we can move an element in a certain direction and what place it will be.
+   *
+   * @param {Object} page the page we want the places for. Usually the current page.
+   * @param {Object} element the element we want the next places for.
+   * @returns an object keyed by the four direction and for each `null` or an object
+   *  containing:
+   * {
+   *   beforeElementId,
+   *   parentElementId,
+   *   placeInContainer,
+   * }
+   * that can be used to move the element.
    */
-  async moveElement(page, element, placement) {
-    if (element.parent_element_id !== null) {
-      const parentElement = this.app.store.getters['element/getElementById'](
-        page,
-        element.parent_element_id
+  getNextPlaces({ builder, page, element }) {
+    let placeInContainer = element.place_in_container
+    const parentElementId = element.parent_element_id
+      ? element.parent_element_id
+      : null
+
+    const elementPage = this.app.store.getters['page/getById'](
+      builder,
+      element.page_id
+    )
+
+    const parentElement = element.parent_element_id
+      ? this.app.store.getters['element/getElementById'](
+          elementPage,
+          element.parent_element_id
+        )
+      : null
+    const parentElementType = parentElement
+      ? this.app.$registry.get('element', parentElement.type)
+      : null
+
+    const elementsAround = this.getElementsAround({ builder, page, element })
+
+    const nextPlaces = {
+      [DIRECTIONS.BEFORE]: null,
+      [DIRECTIONS.AFTER]: null,
+      [DIRECTIONS.LEFT]: null,
+      [DIRECTIONS.RIGHT]: null,
+    }
+
+    // BEFORE
+    const previousElement = elementsAround[DIRECTIONS.BEFORE]
+    if (previousElement) {
+      // If we have a previous element, let place it before it.
+      nextPlaces[DIRECTIONS.BEFORE] = {
+        beforeElementId: previousElement.id,
+        parentElementId,
+        placeInContainer: previousElement.place_in_container,
+      }
+    }
+
+    // AFTER
+    const nextElement = elementsAround[DIRECTIONS.AFTER]
+    if (nextElement) {
+      const nextNextElement = this.app.store.getters['element/getNextElement'](
+        elementPage,
+        nextElement
+      )
+      if (!nextNextElement) {
+        // We have to place this element as last element in the given place
+        nextPlaces[DIRECTIONS.AFTER] = {
+          beforeElementId: null,
+          parentElementId,
+          placeInContainer: element.place_in_container,
+        }
+      } else {
+        // Otherwise it must be placed just before the next next element
+        nextPlaces[DIRECTIONS.AFTER] = {
+          beforeElementId: nextNextElement.id,
+          parentElementId,
+          placeInContainer: nextNextElement.place_in_container,
+        }
+      }
+    }
+
+    // LEFT
+    if (parentElement) {
+      const places = parentElementType.getElementPlaces(parentElement)
+      const placeIndex = places.findIndex(
+        (place) => place === element.place_in_container
       )
 
+      if (placeIndex > 0) {
+        // Let's move it as last of the previous container place
+        nextPlaces[DIRECTIONS.LEFT] = {
+          beforeElementId: null,
+          parentElementId,
+          placeInContainer: places[placeIndex - 1],
+        }
+      }
+    }
+
+    // RIGHT
+    if (parentElement) {
+      const places = parentElementType.getElementPlaces(parentElement)
+      const placeIndex = places.findIndex(
+        (place) => place === element.place_in_container
+      )
+      if (placeIndex < places.length - 1) {
+        placeInContainer = places[placeIndex + 1]
+        const elementsInNextPlace = this.app.store.getters[
+          'element/getElementsInPlace'
+        ](elementPage, element.parent_element_id, placeInContainer)
+        if (elementsInNextPlace.length) {
+          // Let's place it as first element in the next container place
+          nextPlaces[DIRECTIONS.RIGHT] = {
+            beforeElementId: elementsInNextPlace[0].id,
+            parentElementId,
+            placeInContainer,
+          }
+        } else {
+          nextPlaces[DIRECTIONS.RIGHT] = {
+            beforeElementId: null,
+            parentElementId,
+            placeInContainer,
+          }
+        }
+      }
+    }
+    return nextPlaces
+  }
+
+  /**
+   * Returns the elements around the current element in the four directions if the
+   * element exists. The simplified base logic is the following:
+   * - If the element is at root level:
+   *   - The BEFORE element is the previous sibling in the element order
+   *   - The AFTER element is the next sibling in the element order
+   *   - No LEFT nor RIGHT elements.
+   * - If the element is inside container
+   *   - BEFORE and AFTER are the previous and next in order
+   *   - LEFT if the last element of the container previous place if it exists
+   *   - RIGHT is the first element of the container next place if it exists
+   * if `withSharedPage` is true, we consider the header and footer elements as if
+   * they were on the same page respectively before and after the current page elements.
+   * @param {Object} page the current page
+   * @param {Object} element the element we want the elements around
+   * @param {Boolean} withSharedPage whether we want to consider the shared page or not.
+   * @returns an object keyed by the directions and valued by the elements
+   * or null if there is no element in that direction.
+   */
+  getElementsAround({ builder, page, element, withSharedPage = false }) {
+    const elementType = this.app.$registry.get('element', element.type)
+    const elementPlace = elementType.getPagePlace()
+    const isRootElement = !element.parent_element_id
+
+    const elementPage = this.app.store.getters['page/getById'](
+      builder,
+      element.page_id
+    )
+
+    const siblings = this.app.store.getters['element/getElementsInPlace'](
+      elementPage,
+      element.parent_element_id,
+      element.place_in_container
+    ).filter(
+      (sibling) =>
+        Boolean(element.parent_element_id) ||
+        this.app.$registry.get('element', sibling.type).getPagePlace() ===
+          elementPlace
+    )
+
+    const elementIndex = siblings.findIndex((e) => e.id === element.id)
+
+    let previousElement = null
+    let nextElement = null
+
+    if (elementIndex > 0) {
+      previousElement = siblings[elementIndex - 1]
+    }
+
+    if (elementIndex < siblings.length - 1) {
+      nextElement = siblings[elementIndex + 1]
+    }
+
+    // If we are considering the shared page and we have no previous or next element
+    // we want to potentially use the elements from the shared page
+    if (withSharedPage && isRootElement) {
+      const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+
+      if (!previousElement) {
+        // no previous element and we are in the page content, then previous element
+        // could come from the HEADER
+        if (elementPlace === PAGE_PLACES.CONTENT) {
+          const headerElements = this.app.store.getters[
+            'element/getRootElements'
+          ](sharedPage).filter(
+            (element) =>
+              this.app.$registry.get('element', element.type).getPagePlace() ===
+              PAGE_PLACES.HEADER
+          )
+          if (headerElements.length) {
+            previousElement = headerElements.at(-1)
+          }
+        } else if (elementPlace === PAGE_PLACES.FOOTER) {
+          // previous element could come from the page CONTENT if we don't have previous
+          // yet
+          const contentElements =
+            this.app.store.getters['element/getRootElements'](page)
+          if (contentElements.length) {
+            previousElement = contentElements.at(-1)
+          }
+        }
+      }
+
+      // Let's consider the shared page element as next element.
+      // If the current element is in the HEADER, it might be a CONTENT element
+      // If it's a CONTENT element it could be in the FOOTER.
+      if (!nextElement) {
+        if (elementPlace === PAGE_PLACES.HEADER) {
+          const contentElements =
+            this.app.store.getters['element/getRootElements'](page)
+          if (contentElements.length) {
+            nextElement = contentElements[0]
+          }
+        } else if (elementPlace === PAGE_PLACES.CONTENT) {
+          const footerElements = this.app.store.getters[
+            'element/getRootElements'
+          ](sharedPage).filter(
+            (element) =>
+              this.app.$registry.get('element', element.type).getPagePlace() ===
+              PAGE_PLACES.FOOTER
+          )
+          if (footerElements.length) {
+            nextElement = footerElements[0]
+          }
+        }
+      }
+    }
+
+    let leftElement = null
+    let rightElement = null
+
+    // We have a parent, so we can find left and right elements.
+    if (element.parent_element_id) {
+      const parentElement = this.app.store.getters['element/getElementById'](
+        elementPage,
+        element.parent_element_id
+      )
       const parentElementType = this.app.$registry.get(
         'element',
         parentElement.type
       )
-      await parentElementType.moveChildElement(
-        page,
-        parentElement,
-        element,
-        placement
+      const places = parentElementType.getElementPlaces(parentElement)
+      const placeIndex = places.findIndex(
+        (place) => place === element.place_in_container
       )
-    } else {
-      await this.moveElementInSamePlace(page, element, placement)
-    }
-  }
 
-  /**
-   * Identify and select the next element according to the new placement.
-   *
-   * @param {Object} page - The page the element belongs to
-   * @param {Object} element - The element on which the selection should be based on
-   * @param {String} placement - The direction of the selection
-   */
-  async selectNextElement(page, element, placement) {
-    let elementToBeSelected = null
-    if (placement === PLACEMENTS.BEFORE) {
-      elementToBeSelected = this.app.store.getters[
-        'element/getPreviousElement'
-      ](page, element)
-    } else if (placement === PLACEMENTS.AFTER) {
-      elementToBeSelected = this.app.store.getters['element/getNextElement'](
-        page,
-        element
-      )
-    } else {
-      const containerElement = this.app.store.getters['element/getElementById'](
-        page,
-        element.parent_element_id
-      )
-      const containerElementType = this.app.$registry.get(
-        'element',
-        containerElement.type
-      )
-      elementToBeSelected =
-        containerElementType.getNextHorizontalElementToSelect(
-          page,
-          element,
-          placement
-        )
+      let placeLeftIndex = placeIndex - 1
+      while (placeLeftIndex >= 0) {
+        const elementsInNextPlace = this.app.store.getters[
+          'element/getElementsInPlace'
+        ](elementPage, element.parent_element_id, places[placeLeftIndex])
+        if (elementsInNextPlace.length > 0) {
+          leftElement = elementsInNextPlace.at(-1)
+          break
+        }
+        placeLeftIndex -= 1
+      }
+      let placeRightIndex = placeIndex + 1
+      while (placeRightIndex <= places.length - 1) {
+        const elementsInNextPlace = this.app.store.getters[
+          'element/getElementsInPlace'
+        ](elementPage, element.parent_element_id, places[placeRightIndex])
+        if (elementsInNextPlace.length > 0) {
+          rightElement = elementsInNextPlace[0]
+          break
+        }
+        placeRightIndex += 1
+      }
     }
 
-    if (!elementToBeSelected) {
-      return
+    return {
+      [DIRECTIONS.BEFORE]: previousElement,
+      [DIRECTIONS.AFTER]: nextElement,
+      [DIRECTIONS.LEFT]: leftElement,
+      [DIRECTIONS.RIGHT]: rightElement,
     }
-
-    try {
-      await this.app.store.dispatch('element/select', {
-        element: elementToBeSelected,
-      })
-    } catch {}
-  }
-
-  /**
-   * Returns vertical placement disabled.
-   * @param {Object} page - The page the element belongs to
-   * @param {Object} element - The element to move
-   * @param {String} placement - The direction of the move
-   */
-  getVerticalPlacementsDisabled(page, element) {
-    const previousElement = this.app.store.getters[
-      'element/getPreviousElement'
-    ](page, element)
-    const nextElement = this.app.store.getters['element/getNextElement'](
-      page,
-      element
-    )
-
-    const placementsDisabled = []
-
-    if (!previousElement) {
-      placementsDisabled.push(PLACEMENTS.BEFORE)
-    }
-
-    if (!nextElement) {
-      placementsDisabled.push(PLACEMENTS.AFTER)
-    }
-
-    return placementsDisabled
-  }
-
-  /**
-   * Return an array of placements that are disallowed for the element to move
-   * in their container (or root page).
-   *
-   * @param {Object} page The page that is the parent component.
-   * @param {Number} element The element for which the placements should be
-   *  calculated.
-   * @returns {Array} An array of placements that are disallowed for the element.
-   */
-  getPlacementsDisabled(page, element) {
-    // If the element has a parent, let the parent container type derive the
-    // disabled placements.
-    if (element.parent_element_id) {
-      const containerElement = this.app.store.getters['element/getElementById'](
-        page,
-        element.parent_element_id
-      )
-      const elementType = this.app.$registry.get(
-        'element',
-        containerElement.type
-      )
-      return elementType.getPlacementsDisabledForChild(
-        page,
-        containerElement,
-        element
-      )
-    }
-
-    return [
-      PLACEMENTS.LEFT,
-      PLACEMENTS.RIGHT,
-      ...this.getVerticalPlacementsDisabled(page, element),
-    ]
   }
 
   /**
@@ -449,33 +644,6 @@ export class ElementType extends Registerable {
     return {}
   }
 
-  /**
-   * Given an element, iterates over the element's ancestors and finds the
-   * first collection element. An optional function can be passed to map over
-   * each ancestor element.
-   *
-   * @param {Object} page - The page the element belongs to.
-   * @param {Object} element - The element to start the search from.
-   * @param {Function} ancestorMapFn - An optional function which will be
-   * called for each ancestor element, after ensuring it's a collection element.
-   */
-  firstCollectionAncestor(page, element, ancestorMapFn = (element) => true) {
-    const elementType = this.app.$registry.get('element', element.type)
-    if (elementType.isCollectionElement && ancestorMapFn(element)) {
-      return element
-    }
-    const ancestors = this.app.store.getters['element/getAncestors'](
-      page,
-      element
-    )
-    for (const ancestor of ancestors) {
-      const ancestorType = this.app.$registry.get('element', ancestor.type)
-      if (ancestorType.isCollectionElement && ancestorMapFn(ancestor)) {
-        return ancestor
-      }
-    }
-  }
-
   /**
    * Given a `page` and an `element`, and `ancestorType`, returns whether the
    * element has an ancestor of a specified element type.
@@ -492,125 +660,6 @@ export class ElementType extends Registerable {
   }
 }
 
-const ContainerElementTypeMixin = (Base) =>
-  class extends Base {
-    isContainerElement = true
-
-    get elementTypesAll() {
-      return Object.values(this.app.$registry.getAll('element'))
-    }
-
-    /**
-     * Returns an array of element types that are not allowed as children of this element type.
-     * @param {object} page - The page the element belongs to.
-     * @param {Object} element The element in question, it can be used to
-     *  determine in a more dynamic way if specific children are permitted.
-     * @returns {Array} An array of forbidden child element types.
-     */
-    childElementTypesForbidden(page, element) {
-      return []
-    }
-
-    /**
-     * Returns an array of element types that are allowed as children of this element.
-     * If the parent element we're trying to add a child to has a parent, we'll check
-     * each parent until the root element if they have any forbidden element types to
-     * include as well.
-     * @param page
-     * @param element
-     * @returns {Array} An array of permitted child element types.
-     */
-    childElementTypes(page, element) {
-      if (element.parent_element_id) {
-        const parentElement = this.app.store.getters['element/getElementById'](
-          page,
-          element.parent_element_id
-        )
-        const parentElementType = this.app.$registry.get(
-          'element',
-          parentElement.type
-        )
-        return _.difference(
-          parentElementType.childElementTypes(page, parentElement),
-          this.childElementTypesForbidden(page, element)
-        )
-      }
-      return _.difference(
-        this.elementTypesAll,
-        this.childElementTypesForbidden(page, element)
-      )
-    }
-
-    /**
-     * Returns an array of style types that are not allowed as children of this element.
-     * @returns {Array}
-     */
-    get childStylesForbidden() {
-      return []
-    }
-
-    get defaultPlaceInContainer() {
-      throw new Error('Not implemented')
-    }
-
-    /**
-     * Returns the default value when creating a child element to this container.
-     * @param {Object} page The current page object
-     * @param {Object} values The values of the to be created element
-     * @returns the default values for this element.
-     */
-    getDefaultChildValues(page, values) {
-      // By default, an element inside a container should have no left and right padding
-      return { style_padding_left: 0, style_padding_right: 0 }
-    }
-
-    /**
-     * Given a `page` and an `element`, move the child element of a container
-     * in the direction specified by the `placement`.
-     *
-     * The default implementation only supports moving the element vertically.
-     *
-     * @param {Object} page The page that is the parent component.
-     * @param {Number} element The child element to be moved.
-     * @param {String} placement The direction in which the element should move.
-     */
-    async moveChildElement(page, parentElement, element, placement) {
-      if (placement === PLACEMENTS.AFTER || placement === PLACEMENTS.BEFORE) {
-        await this.moveElementInSamePlace(page, element, placement)
-      }
-    }
-
-    /**
-     * Return an array of placements that are disallowed for the elements to move
-     * in their container.
-     *
-     * @param {Object} page The page that is the parent component.
-     * @param {Number} element The child element for which the placements should
-     *    be calculated.
-     * @returns {Array} An array of placements that are disallowed for the element.
-     */
-    getPlacementsDisabledForChild(page, containerElement, element) {
-      this.getPlacementsDisabled(page, element)
-    }
-
-    getNextHorizontalElementToSelect(page, element, placement) {
-      return null
-    }
-
-    /**
-     * A Container element without any child elements is invalid. Return true
-     * if there are no children, otherwise return false.
-     */
-    isInError({ page, element }) {
-      const children = this.app.store.getters['element/getChildren'](
-        page,
-        element
-      )
-
-      return !children.length
-    }
-  }
-
 export class FormContainerElementType extends ContainerElementTypeMixin(
   ElementType
 ) {
@@ -638,17 +687,40 @@ export class FormContainerElementType extends ContainerElementTypeMixin(
     return FormContainerElementForm
   }
 
+  getElementPlaces(element) {
+    return [null]
+  }
+
   /**
-   * Only disallow form containers as nested elements.
-   * @param {object} page - The page the element belongs to.
-   * @param {Object} element The element in question, it can be used to
-   *  determine in a more dynamic way if specific children are permitted.
-   * @returns {Array} An array containing the `FormContainerElementType`.
+   * This element is not allowed in another form container.
    */
-  childElementTypesForbidden(page, element) {
-    return this.elementTypesAll.filter(
-      (elementType) => elementType.type === this.getType()
-    )
+  isDisallowedReason({
+    builder,
+    page,
+    parentElement,
+    beforeElement,
+    placeInContainer,
+    pagePlace,
+  }) {
+    if (parentElement) {
+      const hasSameTypeAncestor = !!this.app.store.getters[
+        'element/getAncestors'
+      ](page, parentElement, {
+        predicate: (ancestor) => ancestor.type === this.type,
+        includeSelf: true,
+      }).length
+      if (hasSameTypeAncestor) {
+        return this.app.i18n.t('elementType.notAllowedInsideSameType')
+      }
+    }
+    return super.isDisallowedReason({
+      builder,
+      page,
+      parentElement,
+      beforeElement,
+      placeInContainer,
+      pagePlace,
+    })
   }
 
   get childStylesForbidden() {
@@ -659,23 +731,6 @@ export class FormContainerElementType extends ContainerElementTypeMixin(
     return [new SubmitEvent({ ...this.app })]
   }
 
-  /**
-   * Return an array of placements that are disallowed for the elements to move
-   * in their container.
-   *
-   * @param {Object} page The page that is the parent component.
-   * @param {Number} element The child element for which the placements should
-   *    be calculated.
-   * @returns {Array} An array of placements that are disallowed for the element.
-   */
-  getPlacementsDisabledForChild(page, containerElement, element) {
-    return [
-      PLACEMENTS.LEFT,
-      PLACEMENTS.RIGHT,
-      ...this.getVerticalPlacementsDisabled(page, element),
-    ]
-  }
-
   /**
    * A form container is invalid if it has no workflow actions, or it has no
    * children.
@@ -718,17 +773,40 @@ export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
     return ColumnElementForm
   }
 
+  getElementPlaces(element) {
+    return [...Array(element.column_amount)].map((_, index) => `${index}`)
+  }
+
   /**
-   * Only disallow column elements as nested elements.
-   * @param {object} page - The page the element belongs to.
-   * @param {Object} element The element in question, it can be used to
-   *  determine in a more dynamic way if specific children are permitted.
-   * @returns {Array} An array containing the `ColumnElementType`.
+   * This element is not allowed in another column container.
    */
-  childElementTypesForbidden(page, element) {
-    return this.elementTypesAll.filter(
-      (elementType) => elementType.type === this.getType()
-    )
+  isDisallowedReason({
+    builder,
+    page,
+    parentElement,
+    beforeElement,
+    placeInContainer,
+    pagePlace,
+  }) {
+    if (parentElement) {
+      const hasSameTypeAncestor = !!this.app.store.getters[
+        'element/getAncestors'
+      ](page, parentElement, {
+        predicate: (ancestor) => ancestor.type === this.type,
+        includeSelf: true,
+      }).length
+      if (hasSameTypeAncestor) {
+        return this.app.i18n.t('elementType.notAllowedInsideSameType')
+      }
+    }
+    return super.isDisallowedReason({
+      builder,
+      page,
+      parentElement,
+      beforeElement,
+      placeInContainer,
+      pagePlace,
+    })
   }
 
   get childStylesForbidden() {
@@ -738,380 +816,8 @@ export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
   get defaultPlaceInContainer() {
     return '0'
   }
-
-  /**
-   * Given a `page` and an `element`, move the child element of a container
-   * in the direction specified by the `placement`.
-   *
-   * @param {Object} page The page that is the parent component.
-   * @param {Number} element The child element to be moved.
-   * @param {String} placement The direction in which the element should move.
-   */
-  async moveChildElement(page, parentElement, element, placement) {
-    if (placement === PLACEMENTS.AFTER || placement === PLACEMENTS.BEFORE) {
-      await super.moveChildElement(page, parentElement, element, placement)
-    } else {
-      const placeInContainer = parseInt(element.place_in_container)
-      const newPlaceInContainer =
-        placement === PLACEMENTS.LEFT
-          ? placeInContainer - 1
-          : placeInContainer + 1
-
-      if (newPlaceInContainer >= 0) {
-        await this.app.store.dispatch('element/move', {
-          page,
-          elementId: element.id,
-          beforeElementId: null,
-          parentElementId: parentElement.id,
-          placeInContainer: `${newPlaceInContainer}`,
-        })
-      }
-    }
-  }
-
-  /**
-   * Return an array of placements that are disallowed for the elements to move
-   * in their container.
-   *
-   * @param {Object} page The page that is the parent component.
-   * @param {Number} element The child element for which the placements should
-   *    be calculated.
-   * @returns {Array} An array of placements that are disallowed for the element.
-   */
-  getPlacementsDisabledForChild(page, containerElement, element) {
-    const columnIndex = parseInt(element.place_in_container)
-
-    const placementsDisabled = []
-
-    if (columnIndex === 0) {
-      placementsDisabled.push(PLACEMENTS.LEFT)
-    }
-
-    if (columnIndex === containerElement.column_amount - 1) {
-      placementsDisabled.push(PLACEMENTS.RIGHT)
-    }
-
-    return [
-      ...placementsDisabled,
-      ...this.getVerticalPlacementsDisabled(page, element),
-    ]
-  }
-
-  getNextHorizontalElementToSelect(page, element, placement) {
-    const offset = placement === PLACEMENTS.LEFT ? -1 : 1
-    const containerElement = this.app.store.getters['element/getElementById'](
-      page,
-      element.parent_element_id
-    )
-
-    let elementsInPlace = []
-    let nextPlaceInContainer = parseInt(element.place_in_container)
-    for (let i = 0; i < containerElement.column_amount; i++) {
-      nextPlaceInContainer += offset
-      elementsInPlace = this.app.store.getters['element/getElementsInPlace'](
-        page,
-        containerElement.id,
-        nextPlaceInContainer.toString()
-      )
-
-      if (elementsInPlace.length) {
-        return elementsInPlace[elementsInPlace.length - 1]
-      }
-    }
-
-    return null
-  }
 }
 
-const CollectionElementTypeMixin = (Base) =>
-  class extends Base {
-    isCollectionElement = true
-
-    /**
-     * A helper function responsible for returning this collection element's
-     * schema properties.
-     */
-    getSchemaProperties(dataSource) {
-      const serviceType = this.app.$registry.get('service', dataSource.type)
-      const schema = serviceType.getDataSchema(dataSource)
-      if (!schema) {
-        return []
-      }
-      return schema.type === 'array'
-        ? schema.items.properties
-        : schema.properties
-    }
-
-    /**
-     * Given a schema property name, is responsible for finding the matching
-     * property option in the element. If it doesn't exist, then we return
-     * an empty object, and it won't be included in the adhoc header.
-     * @param {object} element - the element we want to extract options from.
-     * @param {string} schemaProperty - the schema property name to check.
-     * @returns {object} - the matching property option, or an empty object.
-     */
-    getPropertyOptionsByProperty(element, schemaProperty) {
-      return (
-        element.property_options.find((option) => {
-          return option.schema_property === schemaProperty
-        }) || {}
-      )
-    }
-
-    /**
-     * Responsible for iterating over the schema's properties, filtering
-     * the results down to the properties which are `filterable`, `sortable`,
-     * and `searchable`, and then returning the property value.
-     * @param {string} option - the `filterable`, `sortable` or `searchable`
-     *  property option. If the value is `true` then the property will be
-     *  included in the adhoc header component.
-     * @param {object} element - the element we want to extract options from.
-     * @param {object} dataSource - the dataSource used by `element`.
-     * @returns {array} - an array of schema properties which are present
-     *  in the element's property options where `option` = `true`.
-     */
-    getPropertyOptionByType(option, element, dataSource) {
-      const schemaProperties = dataSource
-        ? this.getSchemaProperties(dataSource)
-        : []
-      return Object.entries(schemaProperties)
-        .filter(
-          ([schemaProperty, _]) =>
-            this.getPropertyOptionsByProperty(element, schemaProperty)[
-              option
-            ] || false
-        )
-        .map(([_, property]) => property)
-    }
-
-    /**
-     * An array of properties within this element which have been flagged
-     * as filterable by the page designer.
-     */
-    adhocFilterableProperties(element, dataSource) {
-      return this.getPropertyOptionByType('filterable', element, dataSource)
-    }
-
-    /**
-     * An array of properties within this element which have been flagged
-     * as sortable by the page designer.
-     */
-    adhocSortableProperties(element, dataSource) {
-      return this.getPropertyOptionByType('sortable', element, dataSource)
-    }
-
-    /**
-     * An array of properties within this element which have been flagged
-     * as searchable by the page designer.
-     */
-    adhocSearchableProperties(element, dataSource) {
-      return this.getPropertyOptionByType('searchable', element, dataSource)
-    }
-
-    /**
-     * By default collection element will load their content at loading time
-     * but if you don't want that you can return false here.
-     */
-    get fetchAtLoad() {
-      return true
-    }
-
-    hasCollectionAncestor(page, element) {
-      return this.app.store.getters['element/getAncestors'](page, element).some(
-        ({ type }) => {
-          const ancestorType = this.app.$registry.get('element', type)
-          return ancestorType.isCollectionElement
-        }
-      )
-    }
-
-    /**
-     * A simple check to return whether this collection element has a
-     * "source of data" (i.e. a data source, or a schema property).
-     * Should not be used as an "in error" or validation check, use
-     * `isInError` for this purpose as it is more thorough.
-     * @param element - The element we want to check for a source of data.
-     * @returns {Boolean} - Whether the element has a source of data.
-     */
-    hasSourceOfData(element) {
-      return Boolean(element.data_source_id || element.schema_property)
-    }
-
-    /**
-     * Collection elements by default will have three permutations of display names:
-     *
-     * 1. If no data source exists, on `element` or its ancestors, then:
-     *   - "Repeat" is returned.
-     * 2. If a data source is found, and `element` has no `schema_property`, then:
-     *   - "Repeat {dataSourceName}" is returned.
-     * 3. If a data source is found, `element` has a `schema_property`, and the integration is Baserow, then:
-     *   - "Repeat {schemaPropertyTitle} ({fieldTypeName})" is returned
-     * 4. If a data source is found, `element` has a `schema_property`, and the integration isn't Baserow, then:
-     *   - "Repeat {schemaPropertyTitle}" is returned
-     * @param element - The element we want to get a display name for.
-     * @param page - The page the element belongs to.
-     * @returns {string} - The display name for the element.
-     */
-    getDisplayName(element, { page, builder }) {
-      let suffix = ''
-
-      const collectionAncestors = this.app.store.getters[
-        'element/getAncestors'
-      ](page, element, {
-        predicate: (ancestor) =>
-          this.app.$registry.get('element', ancestor.type)
-            .isCollectionElement && ancestor.data_source_id !== null,
-      })
-
-      // If the collection element has ancestors, pluck out the first one, which
-      // will have a data source. Otherwise, use `element`, as this element is
-      // the root level element.
-      const collectionElement = collectionAncestors.length
-        ? collectionAncestors[0]
-        : element
-
-      // If we find a collection ancestor which has a data source, we'll
-      // use the data source's name as part of the display name.
-      if (collectionElement?.data_source_id) {
-        const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
-        const dataSource = this.app.store.getters[
-          'dataSource/getPagesDataSourceById'
-        ]([page, sharedPage], collectionElement?.data_source_id)
-        suffix = dataSource ? dataSource.name : ''
-
-        // If we have a data source, and the element has a schema property,
-        // we'll find the property within the data source's schema and pluck
-        // out the title property.
-        if (element.schema_property) {
-          // Find the schema properties. They'll be in different places,
-          // depending on whether this is a list or single row data source.
-          const schemaProperties =
-            dataSource.schema.type === 'array'
-              ? dataSource.schema?.items?.properties
-              : dataSource.schema.properties
-          const schemaField = schemaProperties[element.schema_property]
-          // Only Local/Remote Baserow table schemas will have `original_type`,
-          // which is the `FieldType`. If we find it, we can use it to display
-          // what kind of field type was used.
-          suffix = schemaField?.title || element.schema_property
-          if (schemaField.original_type) {
-            const fieldType = this.app.$registry.get(
-              'field',
-              schemaField.original_type
-            )
-            suffix = `${suffix} (${fieldType.getName()})`
-          }
-        }
-      }
-
-      return suffix ? `${this.name} - ${suffix}` : this.name
-    }
-
-    /**
-     * When a data source is modified or destroyed, we need to ensure that
-     * our collection elements respond accordingly.
-     *
-     * If the data source has been removed, we want to remove it from the
-     * collection element, and then clear its contents from the store.
-     *
-     * If the data source has been updated, we want to trigger a content reset.
-     *
-     * @param event - `ELEMENT_EVENTS.DATA_SOURCE_REMOVED` if a data source
-     *  has been destroyed, or `ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE` if
-     *  it's been updated.
-     * @param params - Context data which the element type can use.
-     */
-    async onElementEvent(event, { builder, element, dataSourceId }) {
-      const page = this.app.store.getters['page/getById'](
-        builder,
-        element.page_id
-      )
-      if (event === ELEMENT_EVENTS.DATA_SOURCE_REMOVED) {
-        if (element.data_source_id === dataSourceId) {
-          // Remove the data_source_id
-          await this.app.store.dispatch('element/forceUpdate', {
-            page,
-            element,
-            values: { data_source_id: null },
-          })
-          // Empty the element content
-          await this.app.store.dispatch('elementContent/clearElementContent', {
-            element,
-          })
-        }
-      }
-      if (event === ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE) {
-        if (element.data_source_id === dataSourceId) {
-          await this.app.store.dispatch(
-            'elementContent/triggerElementContentReset',
-            {
-              element,
-            }
-          )
-        }
-      }
-    }
-
-    /**
-     * A collection element is in error if:
-     *
-     * - No parent (including self) collection elements have a valid data_source_id.
-     * - The parent with the valid data_source_id points to a data_source
-     *   that !returnsList and `schema_property` is blank.
-     * - It is nested in another collection element, and we don't have a `schema_property`.
-     * @param {Object} page - The page the repeat element belongs to.
-     * @param {Object} element - The repeat element
-     * @param {Object} builder - The builder
-     * @returns {Boolean} - Whether the element is in error.
-     */
-    isInError({ page, element, builder }) {
-      // We get all parents with a valid data_source_id
-      const collectionAncestorsWithDataSource = this.app.store.getters[
-        'element/getAncestors'
-      ](page, element, {
-        predicate: (ancestor) =>
-          this.app.$registry.get('element', ancestor.type)
-            .isCollectionElement && ancestor.data_source_id,
-        includeSelf: true,
-      })
-
-      // No parent with a data_source_id means we are in error
-      if (collectionAncestorsWithDataSource.length === 0) {
-        return true
-      }
-
-      // We consider the closest parent collection element with a data_source_id
-      // The closest parent might be the current element itself
-      const parentWithDataSource = collectionAncestorsWithDataSource.at(-1)
-
-      // We now check if the parent element configuration is correct.
-      const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
-      const dataSource = this.app.store.getters[
-        'dataSource/getPagesDataSourceById'
-      ]([page, sharedPage], parentWithDataSource.data_source_id)
-
-      // The data source is missing. May be it has been removed.
-      if (!dataSource) {
-        return true
-      }
-
-      const serviceType = this.app.$registry.get('service', dataSource.type)
-
-      // If the data source type doesn't return a list, we should have a schema_property
-      if (!serviceType.returnsList && !parentWithDataSource.schema_property) {
-        return true
-      }
-
-      // If the current element is not the one with the data source it should have
-      // a schema_property
-      if (parentWithDataSource.id !== element.id && !element.schema_property) {
-        return true
-      }
-
-      return super.isInError({ page, element, builder })
-    }
-  }
-
 export class TableElementType extends CollectionElementTypeMixin(ElementType) {
   static getType() {
     return 'table'
@@ -1205,21 +911,8 @@ export class RepeatElementType extends CollectionElementTypeMixin(
     return RepeatElementForm
   }
 
-  /**
-   * Return an array of placements that are disallowed for the elements to move
-   * in their container.
-   *
-   * @param {Object} page The page that is the parent component.
-   * @param {Number} element The child element for which the placements should
-   *    be calculated.
-   * @returns {Array} An array of placements that are disallowed for the element.
-   */
-  getPlacementsDisabledForChild(page, containerElement, element) {
-    return [
-      PLACEMENTS.LEFT,
-      PLACEMENTS.RIGHT,
-      ...this.getVerticalPlacementsDisabled(page, element),
-    ]
+  getElementPlaces(element) {
+    return [null]
   }
 
   /**
@@ -1290,10 +983,6 @@ export class FormElementType extends ElementType {
     })
   }
 
-  getNextHorizontalElementToSelect(page, element, placement) {
-    return null
-  }
-
   getDataSchema(element) {
     return {
       type: this.formDataType(element),
@@ -1976,7 +1665,7 @@ export class RecordSelectorElementType extends CollectionElementTypeMixin(
   /**
    * This element is a special collection element. It's in error if it's data_source_id
    * is null.
-   * @param {*} param0
+   * @param {Object} element the element to check the error
    * @returns
    */
   isInError({ element }) {
@@ -2075,3 +1764,153 @@ export class DateTimePickerElementType extends FormElementType {
     return this.parseElementDateTime(element, value).isValid()
   }
 }
+
+export class HeaderElementType extends MultiPageElementTypeMixin(
+  ContainerElementTypeMixin(ElementType)
+) {
+  static getType() {
+    return 'header'
+  }
+
+  get name() {
+    return this.app.i18n.t('elementType.header')
+  }
+
+  get description() {
+    return this.app.i18n.t('elementType.headerDescription')
+  }
+
+  get iconClass() {
+    return 'iconoir-align-top-box'
+  }
+
+  get component() {
+    return MultiPageContainerElement
+  }
+
+  get generalFormComponent() {
+    return MultiPageContainerElementForm
+  }
+
+  getPagePlace() {
+    return PAGE_PLACES.HEADER
+  }
+
+  getDefaultChildValues(page, values) {
+    return {}
+  }
+
+  getDefaultValues(page, values) {
+    const superValues = super.getDefaultValues(page, values)
+    return {
+      ...superValues,
+      style_padding_left: 0,
+      style_padding_right: 0,
+    }
+  }
+
+  /**
+   * We can't have this element inside another container. Not allowed outside of HEADER.
+   * We can add id before the first element though.
+   */
+  isDisallowedReason({
+    builder,
+    page,
+    parentElement,
+    beforeElement,
+    placeInContainer,
+    pagePlace,
+  }) {
+    if (parentElement) {
+      // Can't be inserted inside another container
+      return this.app.i18n.t('elementType.notAllowedInsideContainer')
+    }
+
+    const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+
+    if (
+      page.id === sharedPage.id &&
+      pagePlace &&
+      pagePlace !== PAGE_PLACES.HEADER
+    ) {
+      // can't be inserted outside of header
+      return this.app.i18n.t('elementType.notAllowedUnlessHeader')
+    }
+
+    if (page.id !== sharedPage.id) {
+      const orderedElements =
+        this.app.store.getters['element/getElementsOrdered'](page)
+      // Can't be inserted after the first element of the page
+      if (beforeElement && beforeElement.id !== orderedElements[0].id) {
+        return this.app.i18n.t('elementType.notAllowedUnlessTop')
+      }
+    }
+    return null
+  }
+}
+
+export class FooterElementType extends HeaderElementType {
+  static getType() {
+    return 'footer'
+  }
+
+  getPagePlace() {
+    return PAGE_PLACES.FOOTER
+  }
+
+  get name() {
+    return this.app.i18n.t('elementType.footer')
+  }
+
+  get description() {
+    return this.app.i18n.t('elementType.footerDescription')
+  }
+
+  get iconClass() {
+    return 'iconoir-align-bottom-box'
+  }
+
+  get component() {
+    return MultiPageContainerElement
+  }
+
+  get generalFormComponent() {
+    return MultiPageContainerElementForm
+  }
+
+  /**
+   * We can't have this element inside another container. Not allowed outside of FOOTER.
+   * We can add id after the element of the page though.
+   */
+  isDisallowedReason({
+    builder,
+    page,
+    parentElement,
+    beforeElement,
+    placeInContainer,
+    pagePlace,
+  }) {
+    if (parentElement) {
+      // Can't be inserted inside another container
+      return this.app.i18n.t('elementType.notAllowedInsideContainer')
+    }
+
+    const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
+    if (
+      page.id === sharedPage.id &&
+      pagePlace &&
+      pagePlace !== PAGE_PLACES.FOOTER
+    ) {
+      // can't be inserted outside of header
+      return this.app.i18n.t('elementType.notAllowedUnlessFooter')
+    }
+
+    if (page.id !== sharedPage.id) {
+      // Can't be inserted before the end of the page
+      if (beforeElement && beforeElement.page_id !== sharedPage.id) {
+        return this.app.i18n.t('elementType.notAllowedUnlessBottom')
+      }
+    }
+    return null
+  }
+}
diff --git a/web-frontend/modules/builder/enums.js b/web-frontend/modules/builder/enums.js
index b0ca016ad..d49cc97af 100644
--- a/web-frontend/modules/builder/enums.js
+++ b/web-frontend/modules/builder/enums.js
@@ -12,7 +12,7 @@ import {
   UserDataProviderType,
 } from '@baserow/modules/builder/dataProviderTypes'
 
-export const PLACEMENTS = {
+export const DIRECTIONS = {
   BEFORE: 'before',
   AFTER: 'after',
   LEFT: 'left',
@@ -83,6 +83,12 @@ export const BACKGROUND_MODES = {
   FIT: 'fit',
 }
 
+export const PAGE_PLACES = {
+  HEADER: 'header',
+  CONTENT: 'content',
+  FOOTER: 'footer',
+}
+
 export const WIDTH_TYPES = {
   SMALL: { value: 'small', name: 'widthTypes.small' },
   MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
@@ -91,6 +97,12 @@ export const WIDTH_TYPES = {
   FULL_WIDTH: { value: 'full-width', name: 'widthTypes.fullWidth' },
 }
 
+export const SHARE_TYPES = {
+  ALL: 'all',
+  ONLY: 'only',
+  EXCEPT: 'except',
+}
+
 /**
  * A list of all the data providers that can be used in the formula field on the right
  * sidebar in the application builder.
diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json
index ba7903048..6496950a7 100644
--- a/web-frontend/modules/builder/locales/en.json
+++ b/web-frontend/modules/builder/locales/en.json
@@ -76,6 +76,7 @@
   },
   "elementsContext": {
     "searchPlaceholder": "Search elements",
+    "noPageElements": "No elements found for this page",
     "noElements": "No elements found"
   },
   "elementType": {
@@ -110,7 +111,18 @@
     "recordSelector": "Record selector",
     "recordSelectorDescription": "A related record selector",
     "dateTimePicker": "Date time picker",
-    "dateTimePickerDescription": "A date and time input field"
+    "dateTimePickerDescription": "A date and time input field",
+    "header": "Multi-page header",
+    "headerDescription": "A container shared across pages",
+    "footer": "Multi-page footer",
+    "footerDescription": "A container shared across pages",
+    "notAllowedUnlessTop": "This element is allowed only at the top of the page",
+    "notAllowedUnlessBottom": "This element is allowed only at the bottom of the page",
+    "notAllowedUnlessHeader": "This element is allowed only inside the page header",
+    "notAllowedUnlessFooter": "This element is allowed only inside the page footer",
+    "notAllowedInsideContainer": "This element is not allowed inside a container",
+    "notAllowedInsideSameType": "This element is not allowed in a container of the same type",
+    "notAllowedLocation": "This element is not allowed at this location"
   },
   "addElementButton": {
     "label": "Element"
@@ -118,7 +130,7 @@
   "addElementModal": {
     "title": "Add new element",
     "searchPlaceholder": "Search elements",
-    "disabledElementTooltip": "Unavailable inside this element"
+    "elementInProgress": "Adding element..."
   },
   "elementMenu": {
     "moveUp": "Move up",
@@ -142,7 +154,9 @@
     "message": "Click on one of the elements to see more details"
   },
   "pagePreview": {
-    "emptyMessage": "Click to create first element"
+    "emptyMessage": "Click to create an element",
+    "header": "HEADER",
+    "footer": "FOOTER"
   },
   "elementForms": {
     "textInputPlaceholder": "Enter text...",
@@ -375,6 +389,9 @@
     "textName": "Text",
     "numericName": "Numeric"
   },
+  "pageEditor": {
+    "pageNotFound": "Page not found"
+  },
   "publicPage": {
     "siteNotFound": "Site not found",
     "pageNotFound": "Page not found"
@@ -842,7 +859,20 @@
   "dataSourceDropdown": {
     "label": "Data source",
     "noDataSources": "No data sources available",
+    "noSharedDataSources": "No shared data sources available",
     "shared": "shared",
     "pageOnly": "this page"
+  },
+  "multiPageContainerElementForm": {
+    "pagePosition": "Position",
+    "behaviour": "Behaviour",
+    "display": "Display",
+    "selectAll": "Select all",
+    "deselectAll": "Deselect all"
+  },
+  "pageShareType": {
+    "all": "On all pages",
+    "only": "Only on selected pages",
+    "except": "Exclude selected pages"
   }
 }
diff --git a/web-frontend/modules/builder/mixins/collectionElement.js b/web-frontend/modules/builder/mixins/collectionElement.js
index 7c4c4b99c..fa6ee13be 100644
--- a/web-frontend/modules/builder/mixins/collectionElement.js
+++ b/web-frontend/modules/builder/mixins/collectionElement.js
@@ -34,7 +34,7 @@ export default {
       if (!this.element.data_source_id) {
         return null
       }
-      const pages = [this.page, this.sharedPage]
+      const pages = [this.currentPage, this.sharedPage]
       return this.getPagesDataSourceById(pages, this.element.data_source_id)
     },
     dataSourceType() {
@@ -50,10 +50,7 @@ export default {
       return this.getHasMorePage(this.element)
     },
     contentLoading() {
-      return (
-        this.$fetchState.pending ||
-        (this.getLoading(this.element) && !this.elementIsInError)
-      )
+      return this.getLoading(this.element) && !this.elementIsInError
     },
     dispatchContext() {
       return DataProviderType.getAllDataSourceDispatchContext(
@@ -73,7 +70,7 @@ export default {
     },
     elementIsInError() {
       return this.elementType.isInError({
-        page: this.page,
+        page: this.elementPage,
         element: this.element,
         builder: this.builder,
       })
@@ -112,7 +109,7 @@ export default {
   },
   async fetch() {
     if (!this.elementIsInError && this.elementType.fetchAtLoad) {
-      await this.fetchContent([0, this.element.items_per_page], true)
+      await this.fetchContent([0, this.element.items_per_page])
     }
   },
   methods: {
@@ -134,7 +131,7 @@ export default {
       }
       try {
         await this.fetchElementContent({
-          page: this.page,
+          page: this.elementPage,
           element: this.element,
           dataSource: this.dataSource,
           data: this.dispatchContext,
diff --git a/web-frontend/modules/builder/mixins/collectionElementForm.js b/web-frontend/modules/builder/mixins/collectionElementForm.js
index b800e7a60..6b3ef8313 100644
--- a/web-frontend/modules/builder/mixins/collectionElementForm.js
+++ b/web-frontend/modules/builder/mixins/collectionElementForm.js
@@ -1,9 +1,10 @@
 import { mapGetters } from 'vuex'
 import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
 import { CurrentRecordDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
+import elementForm from '@baserow/modules/builder/mixins/elementForm'
 
 export default {
-  mixins: [applicationContextMixin],
+  mixins: [elementForm, applicationContextMixin],
   computed: {
     /**
      * Returns the schema which the service schema property selector
@@ -38,7 +39,7 @@ export default {
     hasCollectionAncestor() {
       const { element } = this.applicationContext
       const elementType = this.$registry.get('element', element.type)
-      return elementType.hasCollectionAncestor(this.page, element)
+      return elementType.hasCollectionAncestor(this.elementPage, element)
     },
     /**
      * In collection element forms, the ability to configure property options
@@ -96,25 +97,47 @@ export default {
       return this.$store.getters['page/getSharedPage'](this.builder)
     },
     /**
-     * Returns all data sources that are available to the current page.
+     * Returns all data sources that are available not on shared page.
      * The data source will need a `type` and a valid schema.
      * @returns {Array} - The data sources the page designer can choose from.
      */
-    dataSources() {
-      const pages = [this.sharedPage, this.page]
-      return this.$store.getters['dataSource/getPagesDataSources'](
-        pages
-      ).filter((dataSource) => {
+    localDataSources() {
+      if (this.elementPage.id === this.sharedPage.id) {
+        // If the element is on the shared page they are no local page but only
+        // shared page.
+        return null
+      } else {
+        return this.$store.getters['dataSource/getPagesDataSources']([
+          this.elementPage,
+        ]).filter((dataSource) => {
+          const serviceType =
+            dataSource.type && this.$registry.get('service', dataSource.type)
+          return serviceType?.getDataSchema(dataSource)
+        })
+      }
+    },
+    /**
+     * Returns the shared data sources.
+     * @returns {Array} - The shared data sources the page designer can choose from.
+     */
+    sharedDataSources() {
+      // We keep only data sources that have a type and a schema.
+      return this.$store.getters['dataSource/getPagesDataSources']([
+        this.sharedPage,
+      ]).filter((dataSource) => {
         const serviceType =
           dataSource.type && this.$registry.get('service', dataSource.type)
         return serviceType?.getDataSchema(dataSource)
       })
     },
+    dataSources() {
+      return [...(this.localDataSources || []), ...this.sharedDataSources]
+    },
     selectedDataSource() {
       if (!this.values.data_source_id) {
         return null
       }
-      const pages = [this.sharedPage, this.page]
+      const pages = [this.sharedPage, this.currentPage]
       return this.$store.getters['dataSource/getPagesDataSourceById'](
         pages,
         this.values.data_source_id
diff --git a/web-frontend/modules/builder/mixins/collectionField.js b/web-frontend/modules/builder/mixins/collectionField.js
index 392aa7570..cc56b6f2a 100644
--- a/web-frontend/modules/builder/mixins/collectionField.js
+++ b/web-frontend/modules/builder/mixins/collectionField.js
@@ -4,7 +4,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
 import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
 
 export default {
-  inject: ['workspace', 'builder', 'page', 'mode'],
+  inject: ['workspace', 'builder', 'elementPage', 'mode'],
   mixins: [element, applicationContextMixin],
   props: {
     element: {
diff --git a/web-frontend/modules/builder/mixins/containerElement.js b/web-frontend/modules/builder/mixins/containerElement.js
index 87fc4b3a4..f152f81e5 100644
--- a/web-frontend/modules/builder/mixins/containerElement.js
+++ b/web-frontend/modules/builder/mixins/containerElement.js
@@ -1,21 +1,20 @@
 import element from '@baserow/modules/builder/mixins/element'
 import { mapGetters } from 'vuex'
-import { PLACEMENTS } from '@baserow/modules/builder/enums'
+import { DIRECTIONS } from '@baserow/modules/builder/enums'
 
 export default {
   mixins: [element],
-  props: {
-    children: {
-      type: Array,
-      required: false,
-      default: () => [],
-    },
-  },
   computed: {
     ...mapGetters({
       elementSelected: 'element/getSelected',
     }),
-    PLACEMENTS: () => PLACEMENTS,
+    DIRECTIONS: () => DIRECTIONS,
+    children() {
+      return this.$store.getters['element/getChildren'](
+        this.elementPage,
+        this.element
+      )
+    },
     elementSelectedId() {
       return this.elementSelected?.id
     },
diff --git a/web-frontend/modules/builder/mixins/element.js b/web-frontend/modules/builder/mixins/element.js
index 02620cf3d..0ab37d5e9 100644
--- a/web-frontend/modules/builder/mixins/element.js
+++ b/web-frontend/modules/builder/mixins/element.js
@@ -5,7 +5,7 @@ import applicationContextMixin from '@baserow/modules/builder/mixins/application
 import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
 
 export default {
-  inject: ['workspace', 'builder', 'page', 'mode'],
+  inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
   mixins: [applicationContextMixin],
   props: {
     element: {
@@ -17,7 +17,7 @@ export default {
     workflowActionsInProgress() {
       const workflowActions = this.$store.getters[
         'workflowAction/getElementWorkflowActions'
-      ](this.page, this.element.id)
+      ](this.elementPage, this.element.id)
       const { recordIndexPath } = this.applicationContext
       const dispatchedById = this.elementType.uniqueElementId(
         this.element,
@@ -38,7 +38,7 @@ export default {
     },
     elementIsInError() {
       return this.elementType.isInError({
-        page: this.page,
+        page: this.elementPage,
         element: this.element,
         builder: this.builder,
       })
@@ -96,7 +96,7 @@ export default {
 
         const workflowActions = this.$store.getters[
           'workflowAction/getElementWorkflowActions'
-        ](this.page, this.element.id).filter(
+        ](this.elementPage, this.element.id).filter(
           ({ event: eventName }) => eventName === event.name
         )
 
diff --git a/web-frontend/modules/builder/mixins/elementForm.js b/web-frontend/modules/builder/mixins/elementForm.js
index 01679d2d8..39c589dc8 100644
--- a/web-frontend/modules/builder/mixins/elementForm.js
+++ b/web-frontend/modules/builder/mixins/elementForm.js
@@ -3,7 +3,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
 import form from '@baserow/modules/core/mixins/form'
 
 export default {
-  inject: ['workspace', 'builder', 'page', 'mode'],
+  inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
   mixins: [form],
   computed: {
     themeConfigBlocks() {
diff --git a/web-frontend/modules/builder/mixins/elementSidePanel.js b/web-frontend/modules/builder/mixins/elementSidePanel.js
index f6443a5a0..2bf280389 100644
--- a/web-frontend/modules/builder/mixins/elementSidePanel.js
+++ b/web-frontend/modules/builder/mixins/elementSidePanel.js
@@ -5,7 +5,18 @@ import { clone } from '@baserow/modules/core/utils/object'
 import { notifyIf } from '@baserow/modules/core/utils/error'
 
 export default {
-  inject: ['workspace', 'builder', 'page'],
+  inject: ['workspace', 'builder', 'applicationContext'],
+  provide() {
+    return {
+      applicationContext: {
+        ...this.applicationContext,
+        element: this.element,
+        page: this.elementPage,
+      },
+      // We add the current element page
+      elementPage: this.elementPage,
+    }
+  },
   computed: {
     ...mapGetters({
       element: 'element/getSelected',
@@ -20,11 +31,19 @@ export default {
 
     parentElement() {
       return this.$store.getters['element/getElementById'](
-        this.page,
+        this.elementPage,
         this.element?.parent_element_id
       )
     },
 
+    elementPage() {
+      // We use the page from the element itself
+      return this.$store.getters['page/getById'](
+        this.builder,
+        this.element.page_id
+      )
+    },
+
     defaultValues() {
       return this.element
     },
@@ -57,7 +76,7 @@ export default {
       if (Object.keys(differences).length > 0) {
         try {
           await this.actionDebouncedUpdateSelectedElement({
-            page: this.page,
+            page: this.elementPage,
             // Here we clone the values to prevent
             // "modification outside of the store" error
             values: clone(differences),
diff --git a/web-frontend/modules/builder/mixins/formElement.js b/web-frontend/modules/builder/mixins/formElement.js
index e72f61a30..290b2356f 100644
--- a/web-frontend/modules/builder/mixins/formElement.js
+++ b/web-frontend/modules/builder/mixins/formElement.js
@@ -29,7 +29,7 @@ export default {
     },
     formElementData() {
       return this.$store.getters['formData/getElementFormEntry'](
-        this.page,
+        this.elementPage,
         this.uniqueElementId
       )
     },
@@ -38,7 +38,7 @@ export default {
     },
     formElementInvalid() {
       return this.$store.getters['formData/getElementInvalid'](
-        this.page,
+        this.elementPage,
         this.uniqueElementId
       )
     },
@@ -52,7 +52,7 @@ export default {
     },
     formElementTouched() {
       return this.$store.getters['formData/getElementTouched'](
-        this.page,
+        this.elementPage,
         this.uniqueElementId
       )
     },
@@ -62,7 +62,7 @@ export default {
      */
     isDescendantOfFormContainer() {
       return this.$store.getters['element/getAncestors'](
-        this.page,
+        this.elementPage,
         this.element
       ).some(({ type }) => type === FormContainerElementType.getType())
     },
@@ -85,7 +85,7 @@ export default {
     },
     setFormData(value) {
       return this.actionSetFormData({
-        page: this.page,
+        page: this.elementPage,
         uniqueElementId: this.uniqueElementId,
         payload: {
           value,
@@ -106,7 +106,7 @@ export default {
      */
     onFormElementTouch() {
       this.$store.dispatch('formData/setElementTouched', {
-        page: this.page,
+        page: this.elementPage,
         wasTouched: true,
         uniqueElementId: this.uniqueElementId,
       })
diff --git a/web-frontend/modules/builder/mixins/formElementForm.js b/web-frontend/modules/builder/mixins/formElementForm.js
index 31d8bb044..3edfa426e 100644
--- a/web-frontend/modules/builder/mixins/formElementForm.js
+++ b/web-frontend/modules/builder/mixins/formElementForm.js
@@ -2,7 +2,7 @@ import elementForm from '@baserow/modules/builder/mixins/elementForm'
 import { DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS } from '@baserow/modules/builder/enums'
 
 export default {
-  inject: ['workspace', 'builder', 'page', 'mode'],
+  inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
   mixins: [elementForm],
   provide() {
     return {
diff --git a/web-frontend/modules/builder/mixins/visibilityForm.js b/web-frontend/modules/builder/mixins/visibilityForm.js
index 942578e24..4be2363e8 100644
--- a/web-frontend/modules/builder/mixins/visibilityForm.js
+++ b/web-frontend/modules/builder/mixins/visibilityForm.js
@@ -1,5 +1,4 @@
 import UserSourceService from '@baserow/modules/core/services/userSource'
-import elementForm from '@baserow/modules/builder/mixins/elementForm'
 
 import {
   DEFAULT_USER_ROLE_PREFIX,
@@ -12,7 +11,7 @@ import {
 } from '@baserow/modules/builder/constants'
 
 export default {
-  mixins: [elementForm],
+  inject: ['builder'],
   data() {
     return {
       allRoles: [],
diff --git a/web-frontend/modules/builder/pages/pageEditor.vue b/web-frontend/modules/builder/pages/pageEditor.vue
index abb21ffdd..0faf74c81 100644
--- a/web-frontend/modules/builder/pages/pageEditor.vue
+++ b/web-frontend/modules/builder/pages/pageEditor.vue
@@ -1,6 +1,6 @@
 <template>
   <div class="page-editor">
-    <PageHeader :page="page" />
+    <PageHeader />
     <div class="layout__col-2-2 page-editor__content">
       <div :style="{ width: `calc(100% - ${panelWidth}px)` }">
         <PagePreview />
@@ -34,7 +34,7 @@ export default {
     return {
       workspace: this.workspace,
       builder: this.builder,
-      page: this.page,
+      currentPage: this.currentPage,
       mode,
       formulaComponent: ApplicationBuilderFormulaInput,
       applicationContext: this.applicationContext,
@@ -92,7 +92,7 @@ export default {
     next()
   },
   layout: 'app',
-  async asyncData({ store, params, error, $registry }) {
+  async asyncData({ store, params, error, $registry, app }) {
     const builderId = parseInt(params.builderId)
     const pageId = parseInt(params.pageId)
 
@@ -115,7 +115,14 @@ export default {
 
       const page = store.getters['page/getById'](builder, pageId)
 
-      await builderApplicationType.loadExtraData(builder, page, mode)
+      if (page.shared) {
+        return error({
+          statusCode: 404,
+          message: app.i18n.t('pageEditor.pageNotFound'),
+        })
+      }
+
+      await builderApplicationType.loadExtraData(builder, mode)
 
       await Promise.all([
         store.dispatch('dataSource/fetch', {
@@ -139,14 +146,17 @@ export default {
 
       data.workspace = workspace
       data.builder = builder
-      data.page = page
+      data.currentPage = page
     } catch (e) {
       // In case of a network error we want to fail hard.
       if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
         throw e
       }
 
-      return error({ statusCode: 404, message: 'page not found.' })
+      return error({
+        statusCode: 404,
+        message: app.i18n.t('pageEditor.pageNotFound'),
+      })
     }
 
     return data
@@ -155,12 +165,13 @@ export default {
     applicationContext() {
       return {
         builder: this.builder,
-        page: this.page,
         mode,
       }
     },
     dataSources() {
-      return this.$store.getters['dataSource/getPageDataSources'](this.page)
+      return this.$store.getters['dataSource/getPageDataSources'](
+        this.currentPage
+      )
     },
     sharedPage() {
       return this.$store.getters['page/getSharedPage'](this.builder)
@@ -173,7 +184,7 @@ export default {
     dispatchContext() {
       return DataProviderType.getAllDataSourceDispatchContext(
         this.$registry.getAll('builderDataProvider'),
-        this.applicationContext
+        { ...this.applicationContext, page: this.currentPage }
       )
     },
     // Separate dispatch context for application level shared data sources
@@ -195,7 +206,7 @@ export default {
         this.$store.dispatch(
           'dataSourceContent/debouncedFetchPageDataSourceContent',
           {
-            page: this.page,
+            page: this.currentPage,
             data: this.dispatchContext,
             mode: this.mode,
           }
@@ -227,7 +238,7 @@ export default {
           this.$store.dispatch(
             'dataSourceContent/debouncedFetchPageDataSourceContent',
             {
-              page: this.page,
+              page: this.currentPage,
               data: newDispatchContext,
               mode: this.mode,
             }
diff --git a/web-frontend/modules/builder/pages/publicPage.vue b/web-frontend/modules/builder/pages/publicPage.vue
index 33f5f3746..18c2063e6 100644
--- a/web-frontend/modules/builder/pages/publicPage.vue
+++ b/web-frontend/modules/builder/pages/publicPage.vue
@@ -3,10 +3,10 @@
     <Toasts></Toasts>
     <PageContent
       v-if="canViewPage"
-      :page="page"
       :path="path"
       :params="params"
       :elements="elements"
+      :shared-elements="sharedElements"
     />
   </div>
 </template>
@@ -45,7 +45,7 @@ export default {
     return {
       workspace: this.workspace,
       builder: this.builder,
-      page: this.page,
+      currentPage: this.currentPage,
       mode: this.mode,
       formulaComponent: ApplicationBuilderFormulaInput,
       applicationContext: this.applicationContext,
@@ -111,6 +111,12 @@ export default {
         store.dispatch('dataSource/fetchPublished', {
           page: sharedPage,
         }),
+        store.dispatch('element/fetchPublished', {
+          page: sharedPage,
+        }),
+        store.dispatch('workflowAction/fetchPublished', {
+          page: sharedPage,
+        }),
       ])
 
       await DataProviderType.initOnceAll(
@@ -170,6 +176,13 @@ export default {
     }
 
     const [pageFound, path, pageParamsValue] = found
+    // Handle 404
+    if (pageFound.shared) {
+      return error({
+        statusCode: 404,
+        message: app.i18n.t('publicPage.pageNotFound'),
+      })
+    }
 
     const page = await store.getters['page/getById'](builder, pageFound.id)
 
@@ -217,7 +230,7 @@ export default {
 
     return {
       builder,
-      page,
+      currentPage: page,
       path,
       params,
       mode,
@@ -226,7 +239,7 @@ export default {
   head() {
     return {
       titleTemplate: '',
-      title: this.page.name,
+      title: this.currentPage.name,
       bodyAttrs: {
         class: 'public-page',
       },
@@ -235,12 +248,11 @@ export default {
   },
   computed: {
     elements() {
-      return this.$store.getters['element/getRootElements'](this.page)
+      return this.$store.getters['element/getRootElements'](this.currentPage)
     },
     applicationContext() {
       return {
         builder: this.builder,
-        page: this.page,
         pageParamsValue: this.params,
         mode: this.mode,
       }
@@ -253,13 +265,13 @@ export default {
       return userCanViewPage(
         this.$store.getters['userSourceUser/getUser'](this.builder),
         this.$store.getters['userSourceUser/isAuthenticated'](this.builder),
-        this.page
+        this.currentPage
       )
     },
     dispatchContext() {
       return DataProviderType.getAllDataSourceDispatchContext(
         this.$registry.getAll('builderDataProvider'),
-        this.applicationContext
+        { ...this.applicationContext, page: this.currentPage }
       )
     },
     // Separate dispatch context for application level data sources
@@ -277,6 +289,9 @@ export default {
         this.sharedPage
       )
     },
+    sharedElements() {
+      return this.$store.getters['element/getRootElements'](this.sharedPage)
+    },
     isAuthenticated() {
       return this.$store.getters['userSourceUser/isAuthenticated'](this.builder)
     },
@@ -307,7 +322,7 @@ export default {
           this.$store.dispatch(
             'dataSourceContent/debouncedFetchPageDataSourceContent',
             {
-              page: this.page,
+              page: this.currentPage,
               data: newDispatchContext,
               mode: this.mode,
             }
@@ -327,16 +342,27 @@ export default {
             {
               page: this.sharedPage,
               data: newDispatchContext,
+              mode: this.mode,
             }
           )
         }
       },
     },
     async isAuthenticated() {
-      // When the user logs in or out, we need to refetch the elements and actions
-      // as they might have changed.
-      this.$store.dispatch('element/fetchPublished', { page: this.page })
-      this.$store.dispatch('workflowAction/fetchPublished', { page: this.page })
+      // When the user login or logout, we need to refetch the elements and actions
+      // as they might have changed
+      await this.$store.dispatch('element/fetchPublished', {
+        page: this.sharedPage,
+      })
+      await this.$store.dispatch('element/fetchPublished', {
+        page: this.currentPage,
+      })
+      await this.$store.dispatch('workflowAction/fetchPublished', {
+        page: this.currentPage,
+      })
+      await this.$store.dispatch('workflowAction/fetchPublished', {
+        page: this.sharedPage,
+      })
 
       // If the user is on a hidden page, redirect them to the Login page if possible.
       await this.maybeRedirectUserToLoginPage()
diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js
index 0a09fc4f2..2d7da12f5 100644
--- a/web-frontend/modules/builder/plugin.js
+++ b/web-frontend/modules/builder/plugin.js
@@ -43,6 +43,8 @@ import {
   IFrameElementType,
   RepeatElementType,
   RecordSelectorElementType,
+  HeaderElementType,
+  FooterElementType,
 } from '@baserow/modules/builder/elementTypes'
 import {
   DesktopDeviceType,
@@ -209,6 +211,8 @@ export default (context) => {
   app.$registry.register('element', new ButtonElementType(context))
   app.$registry.register('element', new TableElementType(context))
   app.$registry.register('element', new ColumnElementType(context))
+  app.$registry.register('element', new HeaderElementType(context))
+  app.$registry.register('element', new FooterElementType(context))
   app.$registry.register('element', new FormContainerElementType(context))
   app.$registry.register('element', new InputTextElementType(context))
   app.$registry.register('element', new ChoiceElementType(context))
diff --git a/web-frontend/modules/builder/store/element.js b/web-frontend/modules/builder/store/element.js
index 9df48435e..564507555 100644
--- a/web-frontend/modules/builder/store/element.js
+++ b/web-frontend/modules/builder/store/element.js
@@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js'
 import ElementService from '@baserow/modules/builder/services/element'
 import PublicBuilderService from '@baserow/modules/builder/services/publishedBuilder'
 import { calculateTempOrder } from '@baserow/modules/core/utils/order'
+import { uuid } from '@baserow/modules/core/utils/string'
 
 const populateElement = (element, registry) => {
   const elementType = registry.get('element', element.type)
@@ -13,6 +14,11 @@ const populateElement = (element, registry) => {
     reset: 0,
     shouldBeFocused: false,
     elementNamespacePath: null,
+    // This uid ensure that when we refresh the elements from the server when we
+    // authenticate that it didn't reuse some of the store values
+    // It breaks collection element reload after authentication for instance
+    // This uid is used as key in the PageElement component
+    uid: uuid(),
     ...elementType.getPopulateStoreProperties(),
   }
 
@@ -202,20 +208,22 @@ const actions = {
       page,
       elementType: elementTypeName,
       beforeId = null,
-      configuration = null,
+      values = null,
       forceCreate = true,
     }
   ) {
     const elementType = this.$registry.get('element', elementTypeName)
+    const updatedValues = elementType.getDefaultValues(page, values)
     const { data: element } = await ElementService(this.$client).create(
       page.id,
       elementTypeName,
       beforeId,
-      elementType.getDefaultValues(page, configuration)
+      updatedValues
     )
 
     if (forceCreate) {
       await dispatch('forceCreate', { page, element })
+
       await dispatch('select', { element })
     }
 
@@ -386,7 +394,11 @@ const actions = {
         dispatch('forceUpdate', {
           page,
           element: elementUpdated,
-          values: { ...elementUpdated },
+          values: {
+            order: elementUpdated.order,
+            place_in_container: elementUpdated.place_in_container,
+            parent_element_id: elementUpdated.parent_element_id,
+          },
         })
       } catch (error) {
         // Restore previous order and place_in_container properties
@@ -436,14 +448,6 @@ const actions = {
       elementType.onElementEvent(event, { element, ...rest })
     })
   },
-  async moveElement({ dispatch, getters }, { page, element, placement }) {
-    const elementType = this.$registry.get('element', element.type)
-    await elementType.moveElement(page, element, placement)
-  },
-  async selectNextElement({ dispatch, getters }, { page, element, placement }) {
-    const elementType = this.$registry.get('element', element.type)
-    await elementType.selectNextElement(page, element, placement)
-  },
   _setElementNamespacePath({ commit, dispatch, getters }, { page, element }) {
     const elementType = this.$registry.get('element', element.type)
     const elementNamespacePath = elementType.getElementNamespacePath(
@@ -470,6 +474,15 @@ const getters = {
     }
     return null
   },
+  getElementByIdInPages: (state, getters) => (pages, id) => {
+    for (const page of pages) {
+      const found = getters.getElementById(page, id)
+      if (found) {
+        return found
+      }
+    }
+    return null
+  },
   getElementsOrdered: (state, getters) => (page) => {
     return page.orderedElements
   },
diff --git a/web-frontend/modules/builder/store/elementContent.js b/web-frontend/modules/builder/store/elementContent.js
index ebdbb783f..090c7e6cb 100644
--- a/web-frontend/modules/builder/store/elementContent.js
+++ b/web-frontend/modules/builder/store/elementContent.js
@@ -183,8 +183,6 @@ const actions = {
       return
     }
 
-    commit('SET_LOADING', { element, value: true })
-
     try {
       if (serviceType.isValid(dataSource)) {
         let rangeToFetch = range
@@ -198,7 +196,6 @@ const actions = {
 
           // Everything is already loaded we can quit now
           if (!rangeToFetch) {
-            commit('SET_LOADING', { element, value: false })
             return
           }
           rangeToFetch = [rangeToFetch[0], rangeToFetch[1] - rangeToFetch[0]]
@@ -209,6 +206,7 @@ const actions = {
           service = PublishedBuilderService
         }
 
+        commit('SET_LOADING', { element, value: true })
         const { data } = await service(this.app.$client).dispatch(
           dataSource.id,
           dispatchContext,
diff --git a/web-frontend/modules/builder/store/page.js b/web-frontend/modules/builder/store/page.js
index 5277af572..0d2d18797 100644
--- a/web-frontend/modules/builder/store/page.js
+++ b/web-frontend/modules/builder/store/page.js
@@ -16,6 +16,7 @@ export function populatePage(page) {
     elementMap: {},
     orderedElements: [],
     workflowActions: [],
+    elementTree: [],
     contents: null,
   }
 }
@@ -41,6 +42,10 @@ const mutations = {
   },
   DELETE_ITEM(state, { builder, id }) {
     const index = builder.pages.findIndex((item) => item.id === id)
+    // Clear the elements to void the page and prevent errors
+    builder.pages[index].elements = []
+    builder.pages[index].elementMap = {}
+    builder.pages[index].orderedElements = []
     builder.pages.splice(index, 1)
   },
   SET_SELECTED(state, { builder, page }) {
diff --git a/web-frontend/modules/builder/workflowActionTypes.js b/web-frontend/modules/builder/workflowActionTypes.js
index ddb7c3d31..140c50a1f 100644
--- a/web-frontend/modules/builder/workflowActionTypes.js
+++ b/web-frontend/modules/builder/workflowActionTypes.js
@@ -130,7 +130,6 @@ export class RefreshDataSourceWorkflowActionType extends WorkflowActionType {
   async execute({ workflowAction, applicationContext }) {
     applicationContext.page.elements
       .filter((element) => {
-        // Only refresh elements that use the data source
         return element.data_source_id === workflowAction.data_source_id
       })
       .map(async (element) => {
diff --git a/web-frontend/modules/core/assets/scss/components/builder/add_element_zone.scss b/web-frontend/modules/core/assets/scss/components/builder/add_element_zone.scss
index 3f24c8fad..71f3a9806 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/add_element_zone.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/add_element_zone.scss
@@ -1,30 +1,41 @@
-.add-element-zone__icon {
-  border: solid 1px $color-neutral-400;
-  border-radius: 100%;
-  padding: 8px;
-}
-
 .add-element-zone {
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 100%;
-  height: 70px;
-  border: dashed 2px $color-neutral-400;
-  cursor: pointer;
+  width: calc(100% - 4px);
+  height: 120px;
+  border: 1px dashed $color-neutral-200;
+  background-color: $color-neutral-10;
+  margin: 2px; // To make sure we have enough space to see the border
 
   @include rounded($rounded-md);
 
   &:hover {
-    color: $color-primary-500;
-    border-color: $color-primary-500;
-
-    .add-element-zone__icon {
-      border-color: $color-primary-500;
-    }
-
-    .add-element-zone__button--disabled {
-      cursor: not-allowed;
-    }
+    border: 1px solid $color-primary-500;
+    background-color: $palette-blue-50;
+    border-color: $palette-blue-300;
+  }
+}
+
+.add-element-zone__button {
+  padding: 10px; // Add more space around to make the click easier
+}
+
+.add-element-zone__icon {
+  font-size: 16px;
+  border: solid 1px $color-neutral-400;
+  border-radius: 100%;
+  padding: 8px;
+  line-height: 1em;
+  cursor: pointer;
+
+  @include elevation($elevation-low);
+
+  .add-element-zone:hover & {
+    color: $color-primary-600;
+  }
+
+  .add-element-zone.add-element-zone--disabled & {
+    cursor: not-allowed;
   }
 }
diff --git a/web-frontend/modules/core/assets/scss/components/builder/all.scss b/web-frontend/modules/core/assets/scss/components/builder/all.scss
index 3acdd6ac7..8814c7f38 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/all.scss
@@ -8,6 +8,7 @@
 @import 'element';
 @import 'element_preview';
 @import 'elements_list';
+@import 'elements_list_item';
 @import 'side_panels';
 @import 'empty_side_panel_state';
 @import 'page_editor';
diff --git a/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss b/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss
index c1460de48..817fd1c30 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss
@@ -70,6 +70,11 @@
       content: '';
       border: solid 1px $color-primary-500;
       pointer-events: none;
+
+      .page__header &,
+      .page__footer & {
+        border-color: $palette-purple-500;
+      }
     }
   }
 
@@ -96,12 +101,18 @@
   @include absolute(-26px, auto, auto, 5px);
   @include rounded($rounded-3xl);
 
+  font-size: 10px;
   background-color: $palette-blue-500;
   color: $white;
   cursor: default;
-  padding: 3px 6px;
+  padding: 4px 8px;
   z-index: 1;
 
+  .page__header &,
+  .page__footer & {
+    background-color: $palette-purple-500;
+  }
+
   .element-preview--first-element & {
     @include absolute(auto, auto, -26px, 5px);
   }
@@ -154,9 +165,10 @@
     border-bottom-right-radius: 3px;
   }
 
-  &.disabled {
+  &--disabled {
     cursor: inherit;
     color: $color-neutral-400;
+    pointer-events: none;
 
     &:hover {
       background-color: $white;
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_table.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_table.scss
index 851272dea..2c4563d3e 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_table.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_table.scss
@@ -102,7 +102,7 @@
     var(--table-cell-horizontal-padding, 20px);
 }
 
-.ab-table__empty-message {
+.ab-table__empty-state {
   text-align: center;
   padding: 50px 0;
   font-weight: 600;
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss
index c33d9ecbb..c7f9436b3 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss
@@ -4,3 +4,4 @@
 @import 'form_container_element';
 @import 'visibility_form';
 @import 'property_option_form';
+@import 'multi_page_container_element_form';
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/multi_page_container_element_form.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/multi_page_container_element_form.scss
new file mode 100644
index 000000000..7ddbbc209
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/multi_page_container_element_form.scss
@@ -0,0 +1,14 @@
+.multi-page-container-element-form__page-list {
+  display: block;
+  margin: 16px 0;
+}
+
+.multi-page-container-element-form__page-checkbox {
+  display: flex;
+  margin-bottom: 12px;
+}
+
+.multi-page-container-element-form__actions {
+  display: flex;
+  gap: 15px;
+}
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/visibility_form.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/visibility_form.scss
index b235e14e8..2a61a4e86 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/visibility_form.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/visibility_form.scss
@@ -3,24 +3,19 @@
   margin-top: 10px;
 }
 
-.visibility-form__role-links {
-  margin-top: 10px;
-  font-size: 12px;
+.visibility-form__role-list {
+  display: block;
+  margin: 16px 0;
 }
 
-.visibility-form__role-links-deselect-all {
-  margin-left: 10px;
-}
-
-.visibility-form__role-checkbox-container {
-  display: inline-block;
-  margin-top: 10px;
-  margin-left: 2px;
-}
-
-.visibility-form__role-checkbox-div {
+.visibility-form__role-checkbox {
   display: flex;
-  margin-bottom: 5px;
+  margin-bottom: 12px;
+}
+
+.visibility-form__actions {
+  display: flex;
+  gap: 15px;
 }
 
 .visibility-form__visibility-all {
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements_context.scss b/web-frontend/modules/core/assets/scss/components/builder/elements_context.scss
index e851856f7..734fcf533 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements_context.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements_context.scss
@@ -1,3 +1,33 @@
 .elements-context {
   width: 300px;
+  padding-right: 4px;
+}
+
+.elements-context__footer {
+  flex: 0 0;
+  border-top: 1px solid $palette-neutral-200;
+  padding: 4px 4px 0;
+}
+
+.elements-context__footer-create {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.elements-context__search-input {
+  display: block;
+  width: 100%;
+  border: none;
+  padding: 0 12px 0 0;
+
+  @include rounded($rounded);
+  @include fixed-height(36px, 12px);
+
+  &::placeholder {
+    color: $palette-neutral-700;
+  }
+}
+
+.elements-context__elements {
+  overflow: auto;
 }
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements_list.scss b/web-frontend/modules/core/assets/scss/components/builder/elements_list.scss
index 11759dbb3..ae3282fce 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements_list.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements_list.scss
@@ -1,151 +1,25 @@
 .elements-list {
-  @extend %context;
-
-  max-width: 360px;
-  display: flex;
-  flex-direction: column;
-}
-
-.elements-list__search-input {
-  display: block;
-  width: 100%;
-  border: none;
-  padding: 0 12px 0 0;
-
-  @include rounded($rounded);
-  @include fixed-height(36px, 12px);
-
-  &::placeholder {
-    color: $palette-neutral-700;
-  }
-}
-
-.elements-list__items {
   position: relative;
   list-style: none;
   padding: 0;
-  margin: 0;
+  margin: 4px 0;
+  padding-bottom: 4px;
   min-height: 0;
-  max-height: (4 * 36px) + 20px; // we show max 4 items
-  // note that the value is `scroll` and not `auto` because it depends on the
-  // v-auto-overflow-scroll directive.
-  overflow-y: scroll;
+  border-bottom: 1px solid $palette-neutral-200;
 
-  &::before,
-  &::after {
-    content: '';
-    display: block;
-    height: 4px;
-    width: 100%;
-  }
-
-  // This class can be set if the max-height is managed by a container element.
-  &.elements-list__items--no-max-height {
-    max-height: none;
-  }
-
-  ul {
-    list-style: none;
-    margin: 0 0 0 20px;
+  .elements-list & {
+    margin: 4px 0 4px 18px;
     padding: 0;
+    border-bottom: none;
+    border-left: 1px solid $palette-neutral-100;
+  }
+
+  .elements-context__elements &:last-child {
+    border-bottom: none;
+  }
+
+  &--empty {
+    padding: 32px 0;
+    text-align: center;
   }
 }
-
-.elements-list__item-label {
-  color: $color-neutral-600;
-  margin: 10px 0 10px 10px;
-  font-size: 14px;
-}
-
-.elements-list__item {
-  position: relative;
-  margin: 0 0 4px 3px;
-  user-select: none;
-
-  @include rounded($rounded);
-
-  &:last-child {
-    margin-bottom: 0;
-  }
-
-  &.elements-list__item--loading::before {
-    content: ' ';
-
-    @include loading(14px);
-    @include absolute(9px, 9px, auto, auto);
-  }
-
-  &.active:not(.elements-list__item--loading) {
-    background-color: rgba($palette-neutral-1300, 0.04);
-  }
-
-  &.disabled {
-    background-color: transparent;
-    cursor: not-allowed;
-
-    &:hover {
-      background-color: transparent;
-    }
-  }
-}
-
-.elements-list__item-link {
-  display: block;
-  color: $palette-neutral-1300;
-  padding: 8px 32px 8px 10px;
-
-  &--selected {
-    background-color: $palette-neutral-100;
-  }
-
-  &:hover {
-    text-decoration: none;
-    background-color: $palette-neutral-100;
-  }
-
-  .elements-list__item.disabled & {
-    color: $palette-neutral-700;
-
-    &:hover {
-      cursor: inherit;
-    }
-  }
-}
-
-.elements-list__item-name {
-  display: flex;
-  align-items: center;
-  font-weight: 500;
-  line-height: 15px;
-  gap: 6px;
-
-  @extend %ellipsis;
-
-  .elements-list__item-link:active & {
-    color: $palette-neutral-900;
-  }
-}
-
-.elements-list__item-name-text {
-  @extend %ellipsis;
-}
-
-.elements-list__item-icon {
-  font-size: 16px;
-
-  .elements-list__item.disabled &,
-  .elements-list__item-link:active & {
-    color: $palette-neutral-900;
-  }
-}
-
-.elements-list__footer {
-  flex: 0 0;
-  border-top: 1px solid $palette-neutral-200;
-  padding: 4px 4px 0;
-}
-
-.elements-list__footer-create {
-  display: flex;
-  flex-wrap: wrap;
-}
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements_list_item.scss b/web-frontend/modules/core/assets/scss/components/builder/elements_list_item.scss
new file mode 100644
index 000000000..7016f3826
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements_list_item.scss
@@ -0,0 +1,90 @@
+.elements-list-item {
+  position: relative;
+  margin: 0;
+  margin-left: 4px;
+  user-select: none;
+
+  @include rounded($rounded);
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+
+  &.elements-list-item--loading::before {
+    content: ' ';
+
+    @include loading(14px);
+    @include absolute(9px, 9px, auto, auto);
+  }
+
+  &.active:not(.elements-list-item--loading) {
+    background-color: rgba($palette-neutral-1300, 0.04);
+  }
+
+  &.disabled {
+    background-color: transparent;
+    cursor: not-allowed;
+
+    &:hover {
+      background-color: transparent;
+    }
+  }
+}
+
+.elements-list-item__link {
+  display: block;
+  color: $palette-neutral-1300;
+  padding: 8px 32px 8px 10px;
+
+  @include rounded($rounded-md);
+
+  .elements-list-item--selected & {
+    background-color: $palette-neutral-100;
+  }
+
+  &:hover {
+    text-decoration: none;
+    background-color: $palette-neutral-100;
+  }
+
+  .elements-list-item.disabled & {
+    color: $palette-neutral-700;
+
+    &:hover {
+      cursor: inherit;
+    }
+  }
+}
+
+.elements-list-item__name {
+  display: flex;
+  align-items: center;
+  font-weight: 500;
+  line-height: 20px;
+  gap: 6px;
+
+  @extend %ellipsis;
+
+  .elements-list-item__link:active & {
+    color: $palette-neutral-900;
+  }
+}
+
+.elements-list-item__name-text {
+  @extend %ellipsis;
+}
+
+.elements-list-item__icon {
+  font-size: 16px;
+  color: $palette-neutral-900;
+
+  .elements-list-item--selected &,
+  .elements-list-item__link:hover & {
+    color: inherit;
+  }
+
+  .elements-list-item.disabled &,
+  .elements-list-item__link:active & {
+    color: $palette-neutral-900;
+  }
+}
diff --git a/web-frontend/modules/core/assets/scss/components/builder/page_preview.scss b/web-frontend/modules/core/assets/scss/components/builder/page_preview.scss
index 2bacdf2ca..7c4b2134d 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/page_preview.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/page_preview.scss
@@ -14,6 +14,22 @@
   width: 100%;
   height: 100%;
   overflow: hidden;
+
+  .page__header,
+  .page__content,
+  .page__footer {
+    // page parts are under the page separators...
+    z-index: 0;
+    position: relative;
+  }
+
+  .page__header--element-selected,
+  .page__content--element-selected,
+  .page__footer--element-selected {
+    // ...Except if they contain the selected element
+    z-index: 2;
+    position: relative;
+  }
 }
 
 .page-preview__add {
@@ -66,3 +82,25 @@
   max-width: 400px;
   margin: 120px auto;
 }
+
+.page-preview__separator {
+  width: 100%;
+  border-bottom: 1px solid $palette-neutral-200;
+  display: flex;
+  justify-content: center;
+  height: 0;
+  position: relative;
+  z-index: 1;
+}
+
+.page-preview__separator-label {
+  position: absolute;
+  top: -8px;
+  padding: 4px 8px;
+  border-radius: 16px;
+  background: $palette-neutral-50;
+  color: $palette-neutral-700;
+  font-size: 8px;
+  font-weight: 700;
+  line-height: 10px;
+}
diff --git a/web-frontend/modules/core/components/workflowActions/WorkflowAction.vue b/web-frontend/modules/core/components/workflowActions/WorkflowAction.vue
index fa57fd013..9afd9ca3b 100644
--- a/web-frontend/modules/core/components/workflowActions/WorkflowAction.vue
+++ b/web-frontend/modules/core/components/workflowActions/WorkflowAction.vue
@@ -39,7 +39,7 @@ export default {
   name: 'WorkflowAction',
   components: { WorkflowActionSelector },
   mixins: [applicationContext],
-  inject: ['page', 'builder', 'mode'],
+  inject: ['builder', 'elementPage', 'mode'],
   props: {
     availableWorkflowActionTypes: {
       type: Array,
@@ -85,7 +85,7 @@ export default {
 
       try {
         await this.actionUpdateWorkflowAction({
-          page: this.page,
+          page: this.elementPage,
           workflowAction: this.workflowAction,
           values,
         })
diff --git a/web-frontend/modules/integrations/localBaserow/mixins/localBaserowService.js b/web-frontend/modules/integrations/localBaserow/mixins/localBaserowService.js
index fa48e4266..43baefeb8 100644
--- a/web-frontend/modules/integrations/localBaserow/mixins/localBaserowService.js
+++ b/web-frontend/modules/integrations/localBaserow/mixins/localBaserowService.js
@@ -1,5 +1,4 @@
 export default {
-  inject: ['page'],
   props: {
     builder: {
       type: Object,
@@ -18,9 +17,6 @@ export default {
     },
   },
   computed: {
-    dataSourceLoading() {
-      return this.$store.getters['dataSource/getLoading'](this.page)
-    },
     /**
      * Used by `LocalBaserowTableServiceConditionalForm` so that when read,
      * we only provide filters which are from untrashed fields. When writing,
@@ -87,12 +83,6 @@ export default {
         this.tableLoading = true
       }
     },
-    dataSourceLoading: {
-      handler() {
-        this.tableLoading = false
-      },
-      immediate: true,
-    },
   },
   methods: {
     /**
diff --git a/web-frontend/test/unit/builder/components/elements/components/ChoiceElement.spec.js b/web-frontend/test/unit/builder/components/elements/components/ChoiceElement.spec.js
index 6234286bd..4e34dc2b5 100644
--- a/web-frontend/test/unit/builder/components/elements/components/ChoiceElement.spec.js
+++ b/web-frontend/test/unit/builder/components/elements/components/ChoiceElement.spec.js
@@ -53,7 +53,8 @@ describe('ChoiceElement', () => {
       },
       provide: {
         builder,
-        page,
+        currentPage: page,
+        elementPage: page,
         mode,
         applicationContext: { builder, page, mode },
         element,
diff --git a/web-frontend/test/unit/builder/components/elements/components/DateTimePickerElement.spec.js b/web-frontend/test/unit/builder/components/elements/components/DateTimePickerElement.spec.js
index 5d3bf65fe..dd1cdd9a6 100644
--- a/web-frontend/test/unit/builder/components/elements/components/DateTimePickerElement.spec.js
+++ b/web-frontend/test/unit/builder/components/elements/components/DateTimePickerElement.spec.js
@@ -85,7 +85,8 @@ describe('DateTimePickerElement', () => {
       },
       provide: {
         builder,
-        page,
+        currentPage: page,
+        elementPage: page,
         mode,
         applicationContext: { builder, page, mode },
         element,
diff --git a/web-frontend/test/unit/builder/components/elements/components/HeadingElement.spec.js b/web-frontend/test/unit/builder/components/elements/components/HeadingElement.spec.js
index 0b0863286..9268943c0 100644
--- a/web-frontend/test/unit/builder/components/elements/components/HeadingElement.spec.js
+++ b/web-frontend/test/unit/builder/components/elements/components/HeadingElement.spec.js
@@ -36,8 +36,9 @@ describe('HeadingElement', () => {
       },
       provide: {
         builder,
-        page,
         mode,
+        currentPage: page,
+        elementPage: page,
         applicationContext: { builder, page, mode },
         element,
         workspace,
@@ -46,7 +47,7 @@ describe('HeadingElement', () => {
     expect(wrapper.element).toMatchSnapshot()
   })
 
-  test('Default HeadingElement component', async () => {
+  test('Default HeadingElement component v2', async () => {
     const builder = { id: 1, theme: { primary_color: '#ccc' } }
     const page = {}
     const workspace = {}
@@ -59,8 +60,9 @@ describe('HeadingElement', () => {
       },
       provide: {
         builder,
-        page,
         mode,
+        currentPage: page,
+        elementPage: page,
         applicationContext: { builder, page, mode },
         element,
         workspace,
diff --git a/web-frontend/test/unit/builder/components/elements/components/RecordSelectorElement.spec.js b/web-frontend/test/unit/builder/components/elements/components/RecordSelectorElement.spec.js
index 2c40d88b2..129cfd077 100644
--- a/web-frontend/test/unit/builder/components/elements/components/RecordSelectorElement.spec.js
+++ b/web-frontend/test/unit/builder/components/elements/components/RecordSelectorElement.spec.js
@@ -68,7 +68,8 @@ describe('RecordSelectorElement', () => {
       },
       provide: {
         builder,
-        page,
+        currentPage: page,
+        elementPage: page,
         mode,
         applicationContext: { builder, page, mode },
         element,
@@ -161,7 +162,8 @@ describe('RecordSelectorElement', () => {
       },
       provide: {
         builder,
-        page,
+        currentPage: page,
+        elementPage: page,
         mode,
         applicationContext: { builder, page, mode, element },
         element,
diff --git a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/HeadingElement.spec.js.snap b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/HeadingElement.spec.js.snap
index f0f9fbec5..54f28f4b2 100644
--- a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/HeadingElement.spec.js.snap
+++ b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/HeadingElement.spec.js.snap
@@ -14,7 +14,7 @@ exports[`HeadingElement Default HeadingElement component 1`] = `
 </div>
 `;
 
-exports[`HeadingElement Default HeadingElement component 2`] = `
+exports[`HeadingElement Default HeadingElement component v2 1`] = `
 <div
   class=""
 >
diff --git a/web-frontend/test/unit/builder/elementTypes.spec.js b/web-frontend/test/unit/builder/elementTypes.spec.js
index 05ecdd615..1f899429e 100644
--- a/web-frontend/test/unit/builder/elementTypes.spec.js
+++ b/web-frontend/test/unit/builder/elementTypes.spec.js
@@ -6,7 +6,6 @@ import {
   RecordSelectorElementType,
 } from '@baserow/modules/builder/elementTypes'
 import { TestApp } from '@baserow/test/helpers/testApp'
-import _ from 'lodash'
 
 import {
   IFRAME_SOURCE_TYPES,
@@ -469,75 +468,138 @@ describe('elementTypes tests', () => {
     })
   })
 
-  describe('elementType childElementTypesForbidden tests', () => {
-    test('FormContainerElementType only itself as a nested child.', () => {
-      const formContainerElementType = testApp
-        .getRegistry()
-        .get('element', 'form_container')
-      const forbiddenChildTypes = formContainerElementType
-        .childElementTypesForbidden({}, {})
-        .map((el) => el.getType())
-      expect(forbiddenChildTypes).toEqual(['form_container'])
-    })
-    test('ColumnElementType forbids only itself as a nested child.', () => {
-      const columnElementType = testApp.getRegistry().get('element', 'column')
-      const forbiddenChildTypes = columnElementType
-        .childElementTypesForbidden({}, {})
-        .map((el) => el.getType())
-      expect(forbiddenChildTypes).toEqual(['column'])
-    })
-    test('RepeatElementType forbids nothing as a child.', () => {
-      const repeatElementType = testApp.getRegistry().get('element', 'repeat')
-      const forbiddenChildTypes = repeatElementType
-        .childElementTypesForbidden({}, {})
-        .map((el) => el.getType())
-      expect(forbiddenChildTypes).toEqual([])
+  describe('elementType isDisallowedReason for base elements', () => {
+    test("Heading can't be placed on header nor footer if before/after another element", () => {
+      const headingElementType = testApp.getRegistry().get('element', 'heading')
+
+      const page = { id: 123 }
+      const sharedPage = { id: 124, shared: true }
+      const anotherMultiPage = {
+        id: 111,
+        type: 'header',
+        page_id: sharedPage.id,
+      }
+      page.elementMap = {}
+      sharedPage.elementMap = { 111: anotherMultiPage }
+
+      expect(
+        headingElementType.isDisallowedReason({
+          builder: { id: 1, pages: [sharedPage, page] },
+          page: sharedPage,
+          parentElement: null,
+          beforeElement: anotherMultiPage,
+          placeInContainer: null,
+          pagePlace: 'header',
+        })
+      ).toEqual('elementType.notAllowedLocation')
+
+      expect(
+        headingElementType.isDisallowedReason({
+          builder: { id: 1, pages: [sharedPage, page] },
+          page: sharedPage,
+          parentElement: null,
+          beforeElement: null,
+          placeInContainer: null,
+          pagePlace: 'footer',
+        })
+      ).toEqual('elementType.notAllowedLocation')
     })
   })
 
-  describe('elementType childElementTypes tests', () => {
-    test('childElementTypes called with element with parent restricts child types using all ancestor childElementTypes requirements.', () => {
-      const page = { id: 1, name: 'Contact Us' }
-      const parentElement = {
-        id: 1,
-        page_id: page.id,
-        parent_element_id: null,
-        type: 'repeat',
-      }
-      const element = {
-        id: 2,
-        page_id: page.id,
-        parent_element_id: parentElement.id,
+  describe('elementType isDisallowedReason tests', () => {
+    test('FormContainerElementType itself as a nested child.', () => {
+      const formContainerElementType = testApp
+        .getRegistry()
+        .get('element', 'form_container')
+
+      const page = { id: 123 }
+      const formAncestor = { id: 111, type: 'form_container', page_id: page.id }
+      const columnAncestor1 = {
+        id: 112,
         type: 'column',
+        page_id: page.id,
+        parent_element_id: 111,
+      }
+      const columnAncestor2 = {
+        id: 113,
+        type: 'column',
+        page_id: page.id,
       }
-      page.elementMap = { 1: parentElement, 2: element }
 
-      const allElementTypes = Object.values(
-        testApp.getRegistry().getAll('element')
-      ).map((el) => el.getType())
+      page.elementMap = {
+        111: formAncestor,
+        112: columnAncestor1,
+        113: columnAncestor2,
+      }
 
-      const columnElementType = testApp.getRegistry().get('element', 'column')
-      const forbiddenColumnChildTypes = columnElementType
-        .childElementTypesForbidden(page, element)
-        .map((el) => el.getType())
+      expect(
+        formContainerElementType.isDisallowedReason({
+          builder: { id: 1 },
+          page,
+          parentElement: formAncestor,
+          beforeElement: null,
+          placeInContainer: 'content',
+        })
+      ).toEqual('elementType.notAllowedInsideSameType')
+      expect(
+        formContainerElementType.isDisallowedReason({
+          builder: { id: 1 },
+          page,
+          parentElement: columnAncestor1,
+          beforeElement: null,
+          placeInContainer: 'content',
+        })
+      ).toEqual('elementType.notAllowedInsideSameType')
+      // We check a top level column element
+      expect(
+        formContainerElementType.isDisallowedReason({
+          builder: { id: 1 },
+          page,
+          parentElement: columnAncestor2,
+          beforeElement: null,
+          placeInContainer: 'content',
+        })
+      ).toEqual(null)
+    })
+    test('ColumnElementType itself as a nested child.', () => {
+      const columnContainerElementType = testApp
+        .getRegistry()
+        .get('element', 'column')
 
-      const repeatElementType = testApp.getRegistry().get('element', 'repeat')
-      const forbiddenRepeatChildTypes = repeatElementType
-        .childElementTypesForbidden(page, {})
-        .map((el) => el.getType())
+      const page = { id: 123 }
+      const columnAncestor = { id: 111, type: 'column', page_id: page.id }
 
-      const allExpectedForbiddenChildTypes = forbiddenColumnChildTypes.concat(
-        forbiddenRepeatChildTypes
-      )
-      const expectedAllowedChildTypes = _.difference(
-        allElementTypes,
-        allExpectedForbiddenChildTypes
-      )
+      page.elementMap = { 111: columnAncestor }
 
-      const childElementTypes = columnElementType
-        .childElementTypes(page, element)
-        .map((el) => el.getType())
-      expect(childElementTypes).toEqual(expectedAllowedChildTypes)
+      expect(
+        columnContainerElementType.isDisallowedReason({
+          builder: { id: 1 },
+          page,
+          parentElement: columnAncestor,
+          beforeElement: null,
+          placeInContainer: 'content',
+        })
+      ).toEqual('elementType.notAllowedInsideSameType')
+    })
+    test('RepeatElementType allow itself as a nested child.', () => {
+      const repeatContainerElementType = testApp
+        .getRegistry()
+        .get('element', 'repeat')
+
+      const page = { id: 123 }
+      const repeatAncestor = { id: 111, type: 'repeat', page_id: page.id }
+
+      page.elementMap = { 111: repeatAncestor }
+
+      expect(
+        repeatContainerElementType.isDisallowedReason({
+          builder: { id: 1 },
+          page,
+          parentElement: repeatAncestor,
+          beforeElement: null,
+          placeInContainer: 'content',
+        })
+      ).toEqual(null)
     })
   })
 
@@ -754,4 +816,239 @@ describe('elementTypes tests', () => {
       expect(elementType.isInError({ page, element })).toBe(false)
     })
   })
+
+  describe.only('elementType elementAround tests', () => {
+    let page, sharedPage, builder
+    beforeEach(async () => {
+      // Populate a page with a bunch of elements
+      page = { id: 123, elements: [], orderedElements: [], elementMap: {} }
+      sharedPage = {
+        id: 124,
+        shared: true,
+        elements: [],
+        orderedElements: [],
+        elementMap: {},
+      }
+      builder = { id: 1, pages: [sharedPage, page] }
+
+      const heading1 = {
+        id: 42,
+        type: 'heading',
+      }
+      const heading2 = {
+        id: 43,
+        type: 'heading',
+      }
+      const elements = [heading1, heading2]
+
+      await Promise.all(
+        elements.map(async (element, index) => {
+          await testApp.getStore().dispatch('element/forceCreate', {
+            page,
+            element: {
+              place_in_container: null,
+              parent_element_id: null,
+              ...element,
+              page_id: page.id,
+              order: `${index}.0000`,
+            },
+          })
+        })
+      )
+
+      const header1 = {
+        id: 111,
+        type: 'header',
+      }
+      const header2 = {
+        id: 112,
+        type: 'header',
+      }
+      const footer1 = {
+        id: 113,
+        type: 'footer',
+      }
+      const footer2 = {
+        id: 114,
+        type: 'footer',
+      }
+
+      const sharedPageElements = [header1, footer1, footer2, header2]
+
+      await Promise.all(
+        sharedPageElements.map(async (element, index) => {
+          await testApp.getStore().dispatch('element/forceCreate', {
+            page: sharedPage,
+            element: {
+              place_in_container: null,
+              parent_element_id: null,
+              ...element,
+              page_id: sharedPage.id,
+              order: `${index}.0000`,
+            },
+          })
+        })
+      )
+    })
+    test('for first header.', () => {
+      const elementType = testApp.getRegistry().get('element', 'header')
+      const firstHeader = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 111)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: firstHeader,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before).toBeNull()
+      expect(elementsAround.after?.id).toEqual(112)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for second header.', () => {
+      const elementType = testApp.getRegistry().get('element', 'header')
+      const secondHeader = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 112)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: secondHeader,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before?.id).toEqual(111)
+      expect(elementsAround.after).toBeNull()
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for second header with sharedPage.', () => {
+      const elementType = testApp.getRegistry().get('element', 'header')
+      const secondHeader = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 112)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: secondHeader,
+        withSharedPage: true,
+      })
+      expect(elementsAround.before?.id).toEqual(111)
+      expect(elementsAround.after?.id).toEqual(42)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for first footer.', () => {
+      const elementType = testApp.getRegistry().get('element', 'footer')
+      const firstFooter = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 113)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: firstFooter,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before).toBeNull()
+      expect(elementsAround.after?.id).toEqual(114)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for first footer with shared page.', () => {
+      const elementType = testApp.getRegistry().get('element', 'footer')
+      const firstFooter = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 113)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: firstFooter,
+        withSharedPage: true,
+      })
+      expect(elementsAround.before?.id).toEqual(43)
+      expect(elementsAround.after?.id).toEqual(114)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for second footer.', () => {
+      const elementType = testApp.getRegistry().get('element', 'footer')
+      const secondFooter = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 114)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: secondFooter,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before?.id).toEqual(113)
+      expect(elementsAround.after).toBeNull()
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for first heading.', () => {
+      const elementType = testApp.getRegistry().get('element', 'heading')
+      const firstHeading = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 42)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: firstHeading,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before).toBeNull()
+      expect(elementsAround.after?.id).toEqual(43)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for second heading.', () => {
+      const elementType = testApp.getRegistry().get('element', 'heading')
+      const secondHeading = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 43)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: secondHeading,
+        withSharedPage: false,
+      })
+      expect(elementsAround.before?.id).toEqual(42)
+      expect(elementsAround.after).toBeNull()
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for first heading with shared page.', () => {
+      const elementType = testApp.getRegistry().get('element', 'heading')
+      const firstHeading = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 42)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: firstHeading,
+        withSharedPage: true,
+      })
+      expect(elementsAround.before?.id).toEqual(112)
+      expect(elementsAround.after?.id).toEqual(43)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+    test('for second heading  with shared page.', () => {
+      const elementType = testApp.getRegistry().get('element', 'heading')
+      const secondHeading = testApp
+        .getStore()
+        .getters['element/getElementByIdInPages']([page, sharedPage], 43)
+      const elementsAround = elementType.getElementsAround({
+        builder,
+        page,
+        element: secondHeading,
+        withSharedPage: true,
+      })
+      expect(elementsAround.before?.id).toEqual(42)
+      expect(elementsAround.after?.id).toEqual(113)
+      expect(elementsAround.left).toBeNull()
+      expect(elementsAround.right).toBeNull()
+    })
+  })
 })