diff --git a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
index 610186679..92cb63c2d 100644
--- a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
+++ b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
@@ -12,8 +12,8 @@ from baserow.contrib.builder.data_sources.exceptions import (
     DataSourceImproperlyConfigured,
 )
 from baserow.contrib.builder.data_sources.handler import DataSourceHandler
-from baserow.contrib.builder.elements.element_types import FormElementType
 from baserow.contrib.builder.elements.handler import ElementHandler
+from baserow.contrib.builder.elements.mixins import FormElementTypeMixin
 from baserow.contrib.builder.elements.models import FormElement
 from baserow.contrib.builder.workflow_actions.handler import (
     BuilderWorkflowActionHandler,
@@ -62,7 +62,7 @@ class FormDataProviderType(DataProviderType):
         """
 
         element: Type[FormElement] = ElementHandler().get_element(element_id)  # type: ignore
-        element_type: FormElementType = element.get_type()  # type: ignore
+        element_type: FormElementTypeMixin = element.get_type()  # type: ignore
         if not element_type.is_valid(element, data_chunk):
             raise FormDataProviderChunkInvalidException(
                 f"Form data {data_chunk} is invalid for its element."
diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py
index 9b288f3da..965a6f82e 100644
--- a/backend/src/baserow/contrib/builder/elements/element_types.py
+++ b/backend/src/baserow/contrib/builder/elements/element_types.py
@@ -1,33 +1,30 @@
 import abc
-from abc import ABC
-from typing import Any, Dict, List, Optional, Type, TypedDict
+from typing import Any, Dict, List, Optional, TypedDict
 
 from django.core.exceptions import ValidationError
 from django.core.validators import validate_email, validate_integer
 from django.db.models import IntegerField, QuerySet
 from django.db.models.functions import Cast
-from django.utils.translation import gettext_lazy as _
 
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError as DRFValidationError
 
-from baserow.api.exceptions import RequestBodyValidationException
 from baserow.contrib.builder.api.elements.serializers import DropdownOptionSerializer
-from baserow.contrib.builder.data_sources.handler import DataSourceHandler
-from baserow.contrib.builder.elements.handler import ElementHandler
+from baserow.contrib.builder.elements.mixins import (
+    CollectionElementTypeMixin,
+    ContainerElementTypeMixin,
+    FormElementTypeMixin,
+)
 from baserow.contrib.builder.elements.models import (
     INPUT_TEXT_TYPES,
     WIDTHS,
     ButtonElement,
     CheckboxElement,
-    CollectionField,
     ColumnElement,
-    ContainerElement,
     DropdownElement,
     DropdownElementOption,
     Element,
     FormContainerElement,
-    FormElement,
     HeadingElement,
     HorizontalAlignments,
     IFrameElement,
@@ -43,7 +40,6 @@ from baserow.contrib.builder.elements.registries import (
     ElementType,
     element_type_registry,
 )
-from baserow.contrib.builder.elements.signals import elements_moved
 from baserow.contrib.builder.formula_importer import import_formula
 from baserow.contrib.builder.pages.handler import PageHandler
 from baserow.contrib.builder.pages.models import Page
@@ -51,301 +47,8 @@ from baserow.contrib.builder.types import ElementDict
 from baserow.core.formula.types import BaserowFormula
 from baserow.core.registry import T
 
-from .registries import collection_field_type_registry
 
-
-class ContainerElementType(ElementType, ABC):
-    # Container element types are imported first.
-    import_element_priority = 2
-
-    @property
-    def child_types_allowed(self) -> List[str]:
-        """
-        Lets you define which children types can be placed inside the container.
-
-        :return: All the allowed children types
-        """
-
-        return [element_type.type for element_type in element_type_registry.get_all()]
-
-    def get_new_place_in_container(
-        self, container_element: ContainerElement, places_removed: List[str]
-    ) -> Optional[str]:
-        """
-        Provides an alternative place that elements can move to when places in the
-        container are removed.
-
-        :param container_element: The container element that has places removed
-        :param places_removed: The places that are being removed
-        :return: The new place in the container the elements can be moved to
-        """
-
-        return None
-
-    def get_places_in_container_removed(
-        self, values: Dict, instance: ContainerElement
-    ) -> List[str]:
-        """
-        This method defines what elements in the container have been removed preceding
-        an update of hte container element.
-
-        :param values: The new values that are being set
-        :param instance: The current state of the element
-        :return: The places in the container that have been removed
-        """
-
-        return []
-
-    def apply_order_by_children(self, queryset: QuerySet[Element]) -> QuerySet[Element]:
-        """
-        Defines the order of the children inside the container.
-
-        :param queryset: The queryset that the order is applied to.
-        :return: A queryset with the order applied to
-        """
-
-        return queryset.order_by("place_in_container", "order")
-
-    def prepare_value_for_db(
-        self, values: Dict, instance: Optional[ContainerElement] = None
-    ):
-        if instance is not None:  # This is an update operation
-            places_removed = self.get_places_in_container_removed(values, instance)
-
-            if len(places_removed) > 0:
-                instances_moved = ElementHandler().before_places_in_container_removed(
-                    instance, places_removed
-                )
-
-                elements_moved.send(self, page=instance.page, elements=instances_moved)
-
-        return super().prepare_value_for_db(values, instance)
-
-    def validate_place_in_container(
-        self, place_in_container: str, instance: ContainerElement
-    ):
-        """
-        Validate that the place in container being set on a child is valid.
-
-        :param place_in_container: The place in container being set
-        :param instance: The instance of the container element
-        :raises DRFValidationError: If the place in container is invalid
-        """
-
-
-class CollectionElementType(ElementType, ABC):
-    allowed_fields = ["data_source", "data_source_id", "items_per_page"]
-    serializer_field_names = ["data_source_id", "fields", "items_per_page"]
-
-    class SerializedDict(ElementDict):
-        data_source_id: int
-        items_per_page: int
-        fields: List[Dict]
-
-    @property
-    def serializer_field_overrides(self):
-        from baserow.contrib.builder.api.elements.serializers import (
-            CollectionFieldSerializer,
-        )
-
-        return {
-            "data_source_id": serializers.IntegerField(
-                allow_null=True,
-                default=None,
-                help_text=TableElement._meta.get_field("data_source").help_text,
-                required=False,
-            ),
-            "items_per_page": serializers.IntegerField(
-                default=20,
-                help_text=TableElement._meta.get_field("items_per_page").help_text,
-                required=False,
-            ),
-            "fields": CollectionFieldSerializer(many=True, required=False),
-        }
-
-    def prepare_value_for_db(
-        self, values: Dict, instance: Optional[LinkElement] = None
-    ):
-        if "data_source_id" in values:
-            data_source_id = values.pop("data_source_id")
-            if data_source_id is not None:
-                data_source = DataSourceHandler().get_data_source(data_source_id)
-                if (
-                    not data_source.service
-                    or not data_source.service.specific.get_type().returns_list
-                ):
-                    raise DRFValidationError(
-                        f"The data source with ID {data_source_id} doesn't return a "
-                        "list."
-                    )
-
-                if instance:
-                    current_page = PageHandler().get_page(instance.page_id)
-                else:
-                    current_page = values["page"]
-
-                if current_page.id != data_source.page_id:
-                    raise RequestBodyValidationException(
-                        {
-                            "data_source_id": [
-                                {
-                                    "detail": "The provided data source doesn't belong "
-                                    "to the same application.",
-                                    "code": "invalid_data_source",
-                                }
-                            ]
-                        }
-                    )
-                values["data_source"] = data_source
-            else:
-                values["data_source"] = None
-
-        return super().prepare_value_for_db(values, instance)
-
-    def after_create(self, instance, values):
-        default_fields = [
-            {
-                "name": _("Column %(count)s") % {"count": 1},
-                "type": "text",
-                "config": {"value": ""},
-            },
-            {
-                "name": _("Column %(count)s") % {"count": 2},
-                "type": "text",
-                "config": {"value": ""},
-            },
-            {
-                "name": _("Column %(count)s") % {"count": 3},
-                "type": "text",
-                "config": {"value": ""},
-            },
-        ]
-
-        fields = values.get("fields", default_fields)
-
-        created_fields = CollectionField.objects.bulk_create(
-            [
-                CollectionField(**field, order=index)
-                for index, field in enumerate(fields)
-            ]
-        )
-        instance.fields.add(*created_fields)
-
-    def after_update(self, instance, values):
-        if "fields" in values:
-            # Remove previous fields
-            instance.fields.all().delete()
-
-            created_fields = CollectionField.objects.bulk_create(
-                [
-                    CollectionField(**field, order=index)
-                    for index, field in enumerate(values["fields"])
-                ]
-            )
-            instance.fields.add(*created_fields)
-
-    def before_delete(self, instance):
-        instance.fields.all().delete()
-
-    def serialize_property(self, element: Element, prop_name: str):
-        """
-        You can customize the behavior of the serialization of a property with this
-        hook.
-        """
-
-        if prop_name == "fields":
-            return [
-                collection_field_type_registry.get(f.type).export_serialized(f)
-                for f in element.fields.all()
-            ]
-
-        return super().serialize_property(element, prop_name)
-
-    def deserialize_property(
-        self,
-        prop_name: str,
-        value: Any,
-        id_mapping: Dict[str, Any],
-        **kwargs,
-    ) -> Any:
-        if prop_name == "data_source_id" and value:
-            return id_mapping["builder_data_sources"][value]
-
-        if prop_name == "fields":
-            return [
-                # We need to add the data_source_id for the current row
-                # provider.
-                collection_field_type_registry.get(f["type"]).import_serialized(
-                    f, id_mapping, data_source_id=kwargs["data_source_id"]
-                )
-                for f in value
-            ]
-
-        return super().deserialize_property(prop_name, value, id_mapping)
-
-    def create_instance_from_serialized(self, serialized_values: Dict[str, Any]):
-        """Deals with the fields"""
-
-        fields = serialized_values.pop("fields", [])
-
-        instance = super().create_instance_from_serialized(serialized_values)
-
-        # Add the field order
-        for i, f in enumerate(fields):
-            f.order = i
-
-        # Create fields
-        created_fields = CollectionField.objects.bulk_create(fields)
-
-        instance.fields.add(*created_fields)
-
-        return instance
-
-    def import_serialized(
-        self,
-        parent: Any,
-        serialized_values: Dict[str, Any],
-        id_mapping: Dict[str, Any],
-        **kwargs,
-    ):
-        """
-        Here we add the data_source_id to the import process to be able to resolve
-        current_record formulas migration.
-        """
-
-        actual_data_source_id = None
-        if serialized_values.get("data_source_id", None):
-            actual_data_source_id = id_mapping["builder_data_sources"][
-                serialized_values["data_source_id"]
-            ]
-
-        return super().import_serialized(
-            parent,
-            serialized_values,
-            id_mapping,
-            data_source_id=actual_data_source_id,
-            **kwargs,
-        )
-
-
-class FormElementType(ElementType):
-    # Form element types are imported second, after containers.
-    import_element_priority = 1
-
-    def is_valid(self, element: Type[FormElement], value: Any) -> bool:
-        """
-        Given an element and form data value, returns whether it's valid.
-        Used by `FormDataProviderType` to determine if form data is valid.
-
-        :param element: The element we're trying to use form data in.
-        :param value: The form data value, which may be invalid.
-        :return: Whether the value is valid or not for this element.
-        """
-
-        return not (element.required and not value)
-
-
-class ColumnElementType(ContainerElementType):
+class ColumnElementType(ContainerElementTypeMixin, ElementType):
     """
     A column element is a container element that can be used to display other elements
     in a column.
@@ -421,6 +124,105 @@ class ColumnElementType(ContainerElementType):
             )
 
 
+class FormContainerElementType(ContainerElementTypeMixin, ElementType):
+    type = "form_container"
+    model_class = FormContainerElement
+    allowed_fields = [
+        "submit_button_label",
+        "button_color",
+        "reset_initial_values_post_submission",
+    ]
+    serializer_field_names = [
+        "submit_button_label",
+        "button_color",
+        "reset_initial_values_post_submission",
+    ]
+
+    class SerializedDict(ElementDict):
+        submit_button_label: BaserowFormula
+        button_color: str
+
+    def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
+        return {"submit_button_label": "'Submit'"}
+
+    @property
+    def serializer_field_overrides(self):
+        from baserow.core.formula.serializers import FormulaSerializerField
+
+        return {
+            "submit_button_label": FormulaSerializerField(
+                help_text=FormContainerElement._meta.get_field(
+                    "submit_button_label"
+                ).help_text,
+                required=False,
+                allow_blank=True,
+                default="",
+            ),
+            "button_color": serializers.CharField(
+                max_length=20,
+                required=False,
+                default="primary",
+                help_text="Button color.",
+            ),
+            "reset_initial_values_post_submission": serializers.BooleanField(
+                help_text=FormContainerElement._meta.get_field(
+                    "reset_initial_values_post_submission"
+                ).help_text,
+                required=False,
+            ),
+        }
+
+    @property
+    def child_types_allowed(self) -> List[str]:
+        child_types_allowed = []
+
+        for element_type in element_type_registry.get_all():
+            if isinstance(element_type, FormElementTypeMixin):
+                child_types_allowed.append(element_type.type)
+
+        return child_types_allowed
+
+    def import_serialized(self, page, serialized_values, id_mapping):
+        serialized_copy = serialized_values.copy()
+        if serialized_copy["submit_button_label"]:
+            serialized_copy["submit_button_label"] = import_formula(
+                serialized_copy["submit_button_label"], id_mapping
+            )
+
+        return super().import_serialized(page, serialized_copy, id_mapping)
+
+
+class TableElementType(CollectionElementTypeMixin, ElementType):
+    type = "table"
+    model_class = TableElement
+
+    class SerializedDict(CollectionElementTypeMixin.SerializedDict):
+        button_color: str
+
+    @property
+    def allowed_fields(self):
+        return super().allowed_fields + ["button_color"]
+
+    @property
+    def serializer_field_names(self):
+        return super().serializer_field_names + ["button_color"]
+
+    @property
+    def serializer_field_overrides(self):
+        return {
+            **super().serializer_field_overrides,
+            "button_color": serializers.CharField(
+                max_length=20,
+                required=False,
+                default="primary",
+                help_text="Button color.",
+            ),
+        }
+
+    def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
+        return {"data_source_id": None}
+
+
 class HeadingElementType(ElementType):
     """
     A simple heading element that can be used to display a title.
@@ -918,7 +720,7 @@ class ImageElementType(ElementType):
         return super().import_serialized(page, serialized_copy, id_mapping)
 
 
-class InputElementType(FormElementType, abc.ABC):
+class InputElementType(FormElementTypeMixin, ElementType, abc.ABC):
     pass
 
 
@@ -1113,105 +915,6 @@ class ButtonElementType(ElementType):
         return super().import_serialized(page, serialized_copy, id_mapping)
 
 
-class TableElementType(CollectionElementType):
-    type = "table"
-    model_class = TableElement
-
-    class SerializedDict(CollectionElementType.SerializedDict):
-        button_color: str
-
-    @property
-    def allowed_fields(self):
-        return super().allowed_fields + ["button_color"]
-
-    @property
-    def serializer_field_names(self):
-        return super().serializer_field_names + ["button_color"]
-
-    @property
-    def serializer_field_overrides(self):
-        return {
-            **super().serializer_field_overrides,
-            "button_color": serializers.CharField(
-                max_length=20,
-                required=False,
-                default="primary",
-                help_text="Button color.",
-            ),
-        }
-
-    def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
-        return {"data_source_id": None}
-
-
-class FormContainerElementType(ContainerElementType):
-    type = "form_container"
-    model_class = FormContainerElement
-    allowed_fields = [
-        "submit_button_label",
-        "button_color",
-        "reset_initial_values_post_submission",
-    ]
-    serializer_field_names = [
-        "submit_button_label",
-        "button_color",
-        "reset_initial_values_post_submission",
-    ]
-
-    class SerializedDict(ElementDict):
-        submit_button_label: BaserowFormula
-        button_color: str
-
-    def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
-        return {"submit_button_label": "'Submit'"}
-
-    @property
-    def serializer_field_overrides(self):
-        from baserow.core.formula.serializers import FormulaSerializerField
-
-        return {
-            "submit_button_label": FormulaSerializerField(
-                help_text=FormContainerElement._meta.get_field(
-                    "submit_button_label"
-                ).help_text,
-                required=False,
-                allow_blank=True,
-                default="",
-            ),
-            "button_color": serializers.CharField(
-                max_length=20,
-                required=False,
-                default="primary",
-                help_text="Button color.",
-            ),
-            "reset_initial_values_post_submission": serializers.BooleanField(
-                help_text=FormContainerElement._meta.get_field(
-                    "reset_initial_values_post_submission"
-                ).help_text,
-                required=False,
-            ),
-        }
-
-    @property
-    def child_types_allowed(self) -> List[str]:
-        child_types_allowed = []
-
-        for element_type in element_type_registry.get_all():
-            if isinstance(element_type, FormElementType):
-                child_types_allowed.append(element_type.type)
-
-        return child_types_allowed
-
-    def import_serialized(self, page, serialized_values, id_mapping):
-        serialized_copy = serialized_values.copy()
-        if serialized_copy["submit_button_label"]:
-            serialized_copy["submit_button_label"] = import_formula(
-                serialized_copy["submit_button_label"], id_mapping
-            )
-
-        return super().import_serialized(page, serialized_copy, id_mapping)
-
-
 class CheckboxElementType(InputElementType):
     type = "checkbox"
     model_class = CheckboxElement
@@ -1270,7 +973,7 @@ class CheckboxElementType(InputElementType):
         }
 
 
-class DropdownElementType(FormElementType):
+class DropdownElementType(FormElementTypeMixin, ElementType):
     type = "dropdown"
     model_class = DropdownElement
     allowed_fields = ["label", "default_value", "required", "placeholder"]
diff --git a/backend/src/baserow/contrib/builder/elements/mixins.py b/backend/src/baserow/contrib/builder/elements/mixins.py
new file mode 100644
index 000000000..4bdcdf80b
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/elements/mixins.py
@@ -0,0 +1,320 @@
+from typing import Any, Dict, List, Optional, Type
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+
+from rest_framework import serializers
+from rest_framework.exceptions import ValidationError as DRFValidationError
+
+from baserow.api.exceptions import RequestBodyValidationException
+from baserow.contrib.builder.data_sources.handler import DataSourceHandler
+from baserow.contrib.builder.elements.handler import ElementHandler
+from baserow.contrib.builder.elements.models import (
+    CollectionField,
+    ContainerElement,
+    Element,
+    FormElement,
+    LinkElement,
+    TableElement,
+)
+from baserow.contrib.builder.elements.registries import (
+    collection_field_type_registry,
+    element_type_registry,
+)
+from baserow.contrib.builder.elements.signals import elements_moved
+from baserow.contrib.builder.pages.handler import PageHandler
+from baserow.contrib.builder.types import ElementDict
+
+
+class ContainerElementTypeMixin:
+    # Container element types are imported first.
+    import_element_priority = 2
+
+    @property
+    def child_types_allowed(self) -> List[str]:
+        """
+        Lets you define which children types can be placed inside the container.
+
+        :return: All the allowed children types
+        """
+
+        return [element_type.type for element_type in element_type_registry.get_all()]
+
+    def get_new_place_in_container(
+        self, container_element: ContainerElement, places_removed: List[str]
+    ) -> Optional[str]:
+        """
+        Provides an alternative place that elements can move to when places in the
+        container are removed.
+
+        :param container_element: The container element that has places removed
+        :param places_removed: The places that are being removed
+        :return: The new place in the container the elements can be moved to
+        """
+
+        return None
+
+    def get_places_in_container_removed(
+        self, values: Dict, instance: ContainerElement
+    ) -> List[str]:
+        """
+        This method defines what elements in the container have been removed preceding
+        an update of hte container element.
+
+        :param values: The new values that are being set
+        :param instance: The current state of the element
+        :return: The places in the container that have been removed
+        """
+
+        return []
+
+    def apply_order_by_children(self, queryset: QuerySet[Element]) -> QuerySet[Element]:
+        """
+        Defines the order of the children inside the container.
+
+        :param queryset: The queryset that the order is applied to.
+        :return: A queryset with the order applied to
+        """
+
+        return queryset.order_by("place_in_container", "order")
+
+    def prepare_value_for_db(
+        self, values: Dict, instance: Optional[ContainerElement] = None
+    ):
+        if instance is not None:  # This is an update operation
+            places_removed = self.get_places_in_container_removed(values, instance)
+
+            if len(places_removed) > 0:
+                instances_moved = ElementHandler().before_places_in_container_removed(
+                    instance, places_removed
+                )
+
+                elements_moved.send(self, page=instance.page, elements=instances_moved)
+
+        return super().prepare_value_for_db(values, instance)
+
+    def validate_place_in_container(
+        self, place_in_container: str, instance: ContainerElement
+    ):
+        """
+        Validate that the place in container being set on a child is valid.
+
+        :param place_in_container: The place in container being set
+        :param instance: The instance of the container element
+        :raises DRFValidationError: If the place in container is invalid
+        """
+
+
+class CollectionElementTypeMixin:
+    allowed_fields = ["data_source", "data_source_id", "items_per_page"]
+    serializer_field_names = ["data_source_id", "fields", "items_per_page"]
+
+    class SerializedDict(ElementDict):
+        data_source_id: int
+        items_per_page: int
+        fields: List[Dict]
+
+    @property
+    def serializer_field_overrides(self):
+        from baserow.contrib.builder.api.elements.serializers import (
+            CollectionFieldSerializer,
+        )
+
+        return {
+            "data_source_id": serializers.IntegerField(
+                allow_null=True,
+                default=None,
+                help_text=TableElement._meta.get_field("data_source").help_text,
+                required=False,
+            ),
+            "items_per_page": serializers.IntegerField(
+                default=20,
+                help_text=TableElement._meta.get_field("items_per_page").help_text,
+                required=False,
+            ),
+            "fields": CollectionFieldSerializer(many=True, required=False),
+        }
+
+    def prepare_value_for_db(
+        self, values: Dict, instance: Optional[LinkElement] = None
+    ):
+        if "data_source_id" in values:
+            data_source_id = values.pop("data_source_id")
+            if data_source_id is not None:
+                data_source = DataSourceHandler().get_data_source(data_source_id)
+                if (
+                    not data_source.service
+                    or not data_source.service.specific.get_type().returns_list
+                ):
+                    raise DRFValidationError(
+                        f"The data source with ID {data_source_id} doesn't return a "
+                        "list."
+                    )
+
+                if instance:
+                    current_page = PageHandler().get_page(instance.page_id)
+                else:
+                    current_page = values["page"]
+
+                if current_page.id != data_source.page_id:
+                    raise RequestBodyValidationException(
+                        {
+                            "data_source_id": [
+                                {
+                                    "detail": "The provided data source doesn't belong "
+                                    "to the same application.",
+                                    "code": "invalid_data_source",
+                                }
+                            ]
+                        }
+                    )
+                values["data_source"] = data_source
+            else:
+                values["data_source"] = None
+
+        return super().prepare_value_for_db(values, instance)
+
+    def after_create(self, instance, values):
+        default_fields = [
+            {
+                "name": _("Column %(count)s") % {"count": 1},
+                "type": "text",
+                "config": {"value": ""},
+            },
+            {
+                "name": _("Column %(count)s") % {"count": 2},
+                "type": "text",
+                "config": {"value": ""},
+            },
+            {
+                "name": _("Column %(count)s") % {"count": 3},
+                "type": "text",
+                "config": {"value": ""},
+            },
+        ]
+
+        fields = values.get("fields", default_fields)
+
+        created_fields = CollectionField.objects.bulk_create(
+            [
+                CollectionField(**field, order=index)
+                for index, field in enumerate(fields)
+            ]
+        )
+        instance.fields.add(*created_fields)
+
+    def after_update(self, instance, values):
+        if "fields" in values:
+            # Remove previous fields
+            instance.fields.all().delete()
+
+            created_fields = CollectionField.objects.bulk_create(
+                [
+                    CollectionField(**field, order=index)
+                    for index, field in enumerate(values["fields"])
+                ]
+            )
+            instance.fields.add(*created_fields)
+
+    def before_delete(self, instance):
+        instance.fields.all().delete()
+
+    def serialize_property(self, element: Element, prop_name: str):
+        """
+        You can customize the behavior of the serialization of a property with this
+        hook.
+        """
+
+        if prop_name == "fields":
+            return [
+                collection_field_type_registry.get(f.type).export_serialized(f)
+                for f in element.fields.all()
+            ]
+
+        return super().serialize_property(element, prop_name)
+
+    def deserialize_property(
+        self,
+        prop_name: str,
+        value: Any,
+        id_mapping: Dict[str, Any],
+        **kwargs,
+    ) -> Any:
+        if prop_name == "data_source_id" and value:
+            return id_mapping["builder_data_sources"][value]
+
+        if prop_name == "fields":
+            return [
+                # We need to add the data_source_id for the current row
+                # provider.
+                collection_field_type_registry.get(f["type"]).import_serialized(
+                    f, id_mapping, data_source_id=kwargs["data_source_id"]
+                )
+                for f in value
+            ]
+
+        return super().deserialize_property(prop_name, value, id_mapping)
+
+    def create_instance_from_serialized(self, serialized_values: Dict[str, Any]):
+        """Deals with the fields"""
+
+        fields = serialized_values.pop("fields", [])
+
+        instance = super().create_instance_from_serialized(serialized_values)
+
+        # Add the field order
+        for i, f in enumerate(fields):
+            f.order = i
+
+        # Create fields
+        created_fields = CollectionField.objects.bulk_create(fields)
+
+        instance.fields.add(*created_fields)
+
+        return instance
+
+    def import_serialized(
+        self,
+        parent: Any,
+        serialized_values: Dict[str, Any],
+        id_mapping: Dict[str, Any],
+        **kwargs,
+    ):
+        """
+        Here we add the data_source_id to the import process to be able to resolve
+        current_record formulas migration.
+        """
+
+        actual_data_source_id = None
+        if (
+            serialized_values.get("data_source_id", None)
+            and "builder_data_sources" in id_mapping
+        ):
+            actual_data_source_id = id_mapping["builder_data_sources"][
+                serialized_values["data_source_id"]
+            ]
+
+        return super().import_serialized(
+            parent,
+            serialized_values,
+            id_mapping,
+            data_source_id=actual_data_source_id,
+            **kwargs,
+        )
+
+
+class FormElementTypeMixin:
+    # Form element types are imported second, after containers.
+    import_element_priority = 1
+
+    def is_valid(self, element: Type[FormElement], value: Any) -> bool:
+        """
+        Given an element and form data value, returns whether it's valid.
+        Used by `FormDataProviderType` to determine if form data is valid.
+
+        :param element: The element we're trying to use form data in.
+        :param value: The form data value, which may be invalid.
+        :return: Whether the value is valid or not for this element.
+        """
+
+        return not (element.required and not value)
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 8fd504295..b1491e83f 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_element_types.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_element_types.py
@@ -5,13 +5,15 @@ from rest_framework.exceptions import ValidationError
 
 from baserow.contrib.builder.elements.element_types import (
     CheckboxElementType,
-    ContainerElementType,
     DropdownElementType,
-    FormElementType,
     IFrameElementType,
     InputTextElementType,
 )
 from baserow.contrib.builder.elements.handler import ElementHandler
+from baserow.contrib.builder.elements.mixins import (
+    ContainerElementTypeMixin,
+    FormElementTypeMixin,
+)
 from baserow.contrib.builder.elements.models import (
     CheckboxElement,
     DropdownElementOption,
@@ -232,18 +234,18 @@ def test_element_type_import_element_priority():
     container_element_types = [
         element_type
         for element_type in element_types
-        if isinstance(element_type, ContainerElementType)
+        if isinstance(element_type, ContainerElementTypeMixin)
     ]
     form_element_types = [
         element_type
         for element_type in element_types
-        if isinstance(element_type, FormElementType)
+        if isinstance(element_type, FormElementTypeMixin)
     ]
     other_element_types = [
         element_type
         for element_type in element_types
-        if not isinstance(element_type, ContainerElementType)
-        and not isinstance(element_type, FormElementType)
+        if not isinstance(element_type, ContainerElementTypeMixin)
+        and not isinstance(element_type, FormElementTypeMixin)
     ]
     manual_ordering = container_element_types + form_element_types + other_element_types
     expected_ordering = sorted(
diff --git a/web-frontend/modules/builder/elementTypes.js b/web-frontend/modules/builder/elementTypes.js
index fcbfb2efe..6efb62474 100644
--- a/web-frontend/modules/builder/elementTypes.js
+++ b/web-frontend/modules/builder/elementTypes.js
@@ -368,61 +368,121 @@ export class ElementType extends Registerable {
   }
 }
 
-export class ContainerElementType extends ElementType {
-  get elementTypesAll() {
-    return Object.values(this.app.$registry.getAll('element'))
-  }
+const ContainerElementTypeMixin = (Base) =>
+  class extends Base {
+    isContainerElementType = true
 
-  /**
-   * Returns an array of element types that are not allowed as children of this element.
-   *
-   * @returns {Array}
-   */
-  get childElementTypesForbidden() {
-    return []
-  }
-
-  get childElementTypes() {
-    return _.difference(this.elementTypesAll, this.childElementTypesForbidden)
-  }
-
-  /**
-   * 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)
+    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.
+     *
+     * @returns {Array}
+     */
+    get childElementTypesForbidden() {
+      return []
+    }
+
+    get childElementTypes() {
+      return _.difference(this.elementTypesAll, this.childElementTypesForbidden)
+    }
+
+    /**
+     * 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
+    }
+  }
+
+export class FormContainerElementType extends ContainerElementTypeMixin(
+  ElementType
+) {
+  static getType() {
+    return 'form_container'
+  }
+
+  get name() {
+    return this.app.i18n.t('elementType.formContainer')
+  }
+
+  get description() {
+    return this.app.i18n.t('elementType.formContainerDescription')
+  }
+
+  get iconClass() {
+    return 'iconoir-frame'
+  }
+
+  get component() {
+    return FormContainerElement
+  }
+
+  get generalFormComponent() {
+    return FormContainerElementForm
+  }
+
+  get childElementTypesForbidden() {
+    return this.elementTypesAll.filter((type) => !type.isFormElement)
+  }
+
+  get events() {
+    return [SubmitEvent]
+  }
+
+  get childStylesForbidden() {
+    return ['style_width']
   }
 
   /**
@@ -435,15 +495,15 @@ export class ContainerElementType extends ElementType {
    * @returns {Array} An array of placements that are disallowed for the element.
    */
   getPlacementsDisabledForChild(page, containerElement, element) {
-    super.getPlacementsDisabled(page, element)
-  }
-
-  getNextHorizontalElementToSelect(page, element, placement) {
-    return null
+    return [
+      PLACEMENTS.LEFT,
+      PLACEMENTS.RIGHT,
+      ...this.getVerticalPlacementsDisabled(page, element),
+    ]
   }
 }
 
-export class ColumnElementType extends ContainerElementType {
+export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
   getType() {
     return 'column'
   }
@@ -470,7 +530,7 @@ export class ColumnElementType extends ContainerElementType {
 
   get childElementTypesForbidden() {
     return this.elementTypesAll.filter(
-      (elementType) => elementType instanceof ContainerElementType
+      (elementType) => elementType.isContainerElementType
     )
   }
 
@@ -566,8 +626,110 @@ export class ColumnElementType extends ContainerElementType {
   }
 }
 
+const CollectionElementTypeMixin = (Base) => class extends Base {}
+
+export class TableElementType extends CollectionElementTypeMixin(ElementType) {
+  getType() {
+    return 'table'
+  }
+
+  get name() {
+    return this.app.i18n.t('elementType.table')
+  }
+
+  get description() {
+    return this.app.i18n.t('elementType.tableDescription')
+  }
+
+  get iconClass() {
+    return 'iconoir-table'
+  }
+
+  get component() {
+    return TableElement
+  }
+
+  get generalFormComponent() {
+    return TableElementForm
+  }
+
+  async onElementEvent(event, { page, element, dataSourceId }) {
+    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,
+          }
+        )
+      }
+    }
+  }
+
+  isInError({ element, builder }) {
+    const collectionFieldsInError = element.fields.map((collectionField) => {
+      const collectionFieldType = this.app.$registry.get(
+        'collectionField',
+        collectionField.type
+      )
+      return collectionFieldType.isInError({
+        field: collectionField,
+        builder,
+      })
+    })
+    return collectionFieldsInError.includes(true)
+  }
+
+  getDisplayName(element, { page }) {
+    let suffix = ''
+
+    if (element.data_source_id) {
+      const dataSource = this.app.store.getters[
+        'dataSource/getPageDataSourceById'
+      ](page, element.data_source_id)
+
+      suffix = dataSource ? ` - ${dataSource.name}` : ''
+    }
+
+    return `${this.name}${suffix}`
+  }
+}
+
+export class RepeatElementType extends ContainerElementTypeMixin(
+  CollectionElementTypeMixin(ElementType)
+) {
+  getType() {
+    return 'repeat'
+  }
+
+  get name() {
+    return this.app.i18n.t('elementType.repeat')
+  }
+
+  get description() {
+    return this.app.i18n.t('elementType.repeatDescription')
+  }
+
+  get iconClass() {
+    return 'iconoir-repeat'
+  }
+}
 /**
- * This class servers as a parent class for all form element types. Form element types
+ * This class serves as a parent class for all form element types. Form element types
  * are all elements that can be used as part of a form. So in simple terms, any element
  * that can represents data in a way that is directly modifiable by an application user.
  */
@@ -918,87 +1080,6 @@ export class ButtonElementType extends ElementType {
   }
 }
 
-export class TableElementType extends ElementType {
-  getType() {
-    return 'table'
-  }
-
-  get name() {
-    return this.app.i18n.t('elementType.table')
-  }
-
-  get description() {
-    return this.app.i18n.t('elementType.tableDescription')
-  }
-
-  get iconClass() {
-    return 'iconoir-table'
-  }
-
-  get component() {
-    return TableElement
-  }
-
-  get generalFormComponent() {
-    return TableElementForm
-  }
-
-  async onElementEvent(event, { page, element, dataSourceId }) {
-    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,
-          }
-        )
-      }
-    }
-  }
-
-  isInError({ element, builder }) {
-    const collectionFieldsInError = element.fields.map((collectionField) => {
-      const collectionFieldType = this.app.$registry.get(
-        'collectionField',
-        collectionField.type
-      )
-      return collectionFieldType.isInError({
-        field: collectionField,
-        builder,
-      })
-    })
-    return collectionFieldsInError.includes(true)
-  }
-
-  getDisplayName(element, { page }) {
-    let suffix = ''
-
-    if (element.data_source_id) {
-      const dataSource = this.app.store.getters[
-        'dataSource/getPageDataSourceById'
-      ](page, element.data_source_id)
-
-      suffix = dataSource ? ` - ${dataSource.name}` : ''
-    }
-
-    return `${this.name}${suffix}`
-  }
-}
-
 export class DropdownElementType extends FormElementType {
   static getType() {
     return 'dropdown'
@@ -1064,61 +1145,6 @@ export class DropdownElementType extends FormElementType {
   }
 }
 
-export class FormContainerElementType extends ContainerElementType {
-  static getType() {
-    return 'form_container'
-  }
-
-  get name() {
-    return this.app.i18n.t('elementType.formContainer')
-  }
-
-  get description() {
-    return this.app.i18n.t('elementType.formContainerDescription')
-  }
-
-  get iconClass() {
-    return 'iconoir-frame'
-  }
-
-  get component() {
-    return FormContainerElement
-  }
-
-  get generalFormComponent() {
-    return FormContainerElementForm
-  }
-
-  get childElementTypesForbidden() {
-    return this.elementTypesAll.filter((type) => !type.isFormElement)
-  }
-
-  get events() {
-    return [SubmitEvent]
-  }
-
-  get childStylesForbidden() {
-    return ['style_width']
-  }
-
-  /**
-   * 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),
-    ]
-  }
-}
-
 export class CheckboxElementType extends FormElementType {
   getType() {
     return 'checkbox'
diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json
index b7334c1d6..570917c4b 100644
--- a/web-frontend/modules/builder/locales/en.json
+++ b/web-frontend/modules/builder/locales/en.json
@@ -93,7 +93,9 @@
     "iframe": "IFrame",
     "iframeDescription": "Inline frame",
     "authForm": "Login form",
-    "authFormDescription": "A user login form"
+    "authFormDescription": "A user login form",
+    "repeat": "Repeat",
+    "repeatDescription": "A repeatable set of elements"
   },
   "addElementButton": {
     "label": "Element"
diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js
index 6e072c798..5f55c2436 100644
--- a/web-frontend/modules/builder/plugin.js
+++ b/web-frontend/modules/builder/plugin.js
@@ -38,6 +38,7 @@ import {
   DropdownElementType,
   CheckboxElementType,
   IFrameElementType,
+  RepeatElementType,
 } from '@baserow/modules/builder/elementTypes'
 import {
   DesktopDeviceType,
@@ -171,6 +172,10 @@ export default (context) => {
   app.$registry.register('element', new DropdownElementType(context))
   app.$registry.register('element', new CheckboxElementType(context))
 
+  if (app.$featureFlagIsEnabled('builder-repeat-element')) {
+    app.$registry.register('element', new RepeatElementType(context))
+  }
+
   app.$registry.register('device', new DesktopDeviceType(context))
   app.$registry.register('device', new TabletDeviceType(context))
   app.$registry.register('device', new SmartphoneDeviceType(context))